/*-------------------------------------------------------------------------
 *
 * autovacuum.c
 *
 * PostgreSQL 集成的自动真空守护进程
 *
 * 自动真空系统由两种不同类型的进程结构：自动真空发起程序和自动真空工作进程。
 * 发起程序是一个一直运行的进程，在自动真空 GUC 参数设置时由 postmaster 启动。
 * 发起程序在适当的时候调度自动真空工作进程的启动。工作进程是执行实际
 * 真空操作的进程；它们连接到由发起程序确定的数据库，并且一旦连接，它们检查
 * 目录以选择需要真空的表。
 *
 * 自动真空发起程序不能单独启动工作进程，
 * 因为这样做会导致鲁棒性问题（即，在异常条件下无法关闭它们；此外，由于
 * 发起程序连接到共享内存并因此可能会在这里遭到损坏，其鲁棒性不如 postmaster）。
 * 因此，它将此任务留给 postmaster。
 *
 * 有一个自动真空共享内存区域，发起程序在其中存储有关
 * 其希望进行真空的数据库的信息。当它希望启动一个新的工作进程时，
 * 它在共享内存中设置一个标志并向 postmaster 发送信号。 然后，
 * postmaster 知道它必须启动一个工作进程；因此，它派生出一个新的子进程，
 * 该进程变成工作进程。这个新进程连接到共享内存，并能查看发起程序设置的信息。
 *
 * 如果 postmaster 中的 fork() 调用失败，它会在共享内存区域
 * 中设置一个标志，并向发起程序发送信号。 发起程序在注意到该标志后，
 * 可以通过重新发送信号来尝试再次启动工作进程。请注意，失败只能是短暂的
 * （由于高负载、内存压力、进程过多等导致的 fork 失败）；更永久性的问题
 * ，例如无法连接到数据库，将在工作进程中稍后检测到，并仅通过让工作
 * 进程正常退出来处理。发起程序将按计划稍后再次启动新的工作进程。
 *
 * 当工作进程完成真空操作时，它会向发起程序发送 SIGUSR2。
 * 发起程序会醒来，并能够启动另一个工作进程，如果计划如此紧凑，需要
 * 立即启动新的工作进程。在此时，发起程序还可以平衡各个剩余工作进程的
 * 基于费用的真空延迟特性设置。
 *
 * 请注意，在一个数据库中可以有多个工作进程同时运行。
 * 它们会在共享内存中存储当前正在真空的表，以便其他工作进程避免在等待
 * 该表的真空锁时被阻塞。它们还会在真空每个表之前从 pgstats 获取
 * 表的最后真空时间，以避免真空一个刚刚被另一个工作进程真空过的表，从而
 * 使该表在共享内存中不再被记录。然而，由于尚未持有关系锁，
 * 在这段小时间窗口内，工作进程可能会选择一个已经被真空过的表；这是
 * 当前设计中的一个bug。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/postmaster/autovacuum.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <signal.h>
#include <sys/time.h>
#include <unistd.h>

#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/multixact.h"
#include "access/reloptions.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/xact.h"
#include "catalog/dependency.h"
#include "catalog/namespace.h"
#include "catalog/pg_database.h"
#include "catalog/pg_namespace.h"
#include "commands/dbcommands.h"
#include "commands/vacuum.h"
#include "lib/ilist.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "pgstat.h"
#include "postmaster/autovacuum.h"
#include "postmaster/fork_process.h"
#include "postmaster/interrupt.h"
#include "postmaster/postmaster.h"
#include "storage/bufmgr.h"
#include "storage/ipc.h"
#include "storage/latch.h"
#include "storage/lmgr.h"
#include "storage/pmsignal.h"
#include "storage/proc.h"
#include "storage/procsignal.h"
#include "storage/sinvaladt.h"
#include "storage/smgr.h"
#include "tcop/tcopprot.h"
#include "utils/fmgroids.h"
#include "utils/fmgrprotos.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/ps_status.h"
#include "utils/rel.h"
#include "utils/snapmgr.h"
#include "utils/syscache.h"
#include "utils/timeout.h"
#include "utils/timestamp.h"


/*
 * GUC参数
 */
bool		autovacuum_start_daemon = false;
int			autovacuum_max_workers;
int			autovacuum_work_mem = -1;
int			autovacuum_naptime;
int			autovacuum_vac_thresh;
double		autovacuum_vac_scale;
int			autovacuum_vac_ins_thresh;
double		autovacuum_vac_ins_scale;
int			autovacuum_anl_thresh;
double		autovacuum_anl_scale;
int			autovacuum_freeze_max_age;
int			autovacuum_multixact_freeze_max_age;

double		autovacuum_vac_cost_delay;
int			autovacuum_vac_cost_limit;

int			Log_autovacuum_min_duration = 600000;

/* 两次唤醒启动器之间允许的最短时间 */
#define MIN_AUTOVAC_SLEEPTIME 100.0 /* 毫秒 */
#define MAX_AUTOVAC_SLEEPTIME 300	/* seconds */

/* 标志以指示我们是否在自动真空处理过程中 */
static bool am_autovacuum_launcher = false;
static bool am_autovacuum_worker = false;

/* 信号处理程序设置的标志 */
static volatile sig_atomic_t got_SIGUSR2 = false;

/* 用于确定是否超过freeze_max_age的比较点 */
static TransactionId recentXid;
static MultiXactId recentMulti;

/* 用于自动真空的默认冻结年龄（因数据库而异） */
static int	default_freeze_min_age;
static int	default_freeze_table_age;
static int	default_multixact_freeze_min_age;
static int	default_multixact_freeze_table_age;

/* 用于长期数据的内存上下文 */
static MemoryContext AutovacMemCxt;

/* 用于跟踪启动器中数据库的结构 */
typedef struct avl_dbase
{
	Oid			adl_datid;		/* 哈希键——必须是第一个 */
	TimestampTz adl_next_worker;
	int			adl_score;
	dlist_node	adl_node;
} avl_dbase;

/* 用于跟踪工作进程中数据库的结构 */
typedef struct avw_dbase
{
	Oid			adw_datid;
	char	   *adw_name;
	TransactionId adw_frozenxid;
	MultiXactId adw_minmulti;
	PgStat_StatDBEntry *adw_entry;
} avw_dbase;

/* 用于跟踪需要真空和/或分析的表的结构，在第一次通过中 */
typedef struct av_relation
{
	Oid			ar_toastrelid;	/* 哈希键 - 必须放在第一位 */
	Oid			ar_relid;
	bool		ar_hasrelopts;
	AutoVacOpts ar_reloptions;	/*来自主表的AutoVacOpts的副本
								 * reloptions，如果没有则为NULL */
} av_relation;

/* 结构体用于跟踪需要执行真空和/或分析的表，在重新检查后 */
typedef struct autovac_table
{
	Oid			at_relid;
	VacuumParams at_params;
	double		at_vacuum_cost_delay;
	int			at_vacuum_cost_limit;
	bool		at_dobalance;
	bool		at_sharedrel;
	char	   *at_relname;
	char	   *at_nspname;
	char	   *at_datname;
} autovac_table;

/*-------------
 * 此结构体保存关于单个工作者所在地的信息。我们在共享内存中保持
 * 这些数组，其大小由 autovacuum_max_workers 决定。
 *
 * wi_links		进入空闲列表或运行列表
 * wi_dboid		此工作者应处理的数据库的 OID
 * wi_tableoid	当前正在进行真空的表的 OID（如果有的话）
 * wi_sharedrel 用于指示表是否标记为 relisshared 的标志
 * wi_proc		运行工作者的 PGPROC 指针，如果尚未启动则为 NULL
 * wi_launchtime 此工作者启动的时间
 * wi_cost_*	此工作者当前的基于成本的真空延迟参数
 *
 * 所有字段都受到 AutovacuumLock 保护，wi_tableoid 和
 * wi_sharedrel 除外，它们受 AutovacuumScheduleLock 保护（请注意，这
 * 两个字段对除了该工作者本身以外的所有人都是只读的）。
 *-------------
 */
typedef struct WorkerInfoData
{
	dlist_node	wi_links;
	Oid			wi_dboid;
	Oid			wi_tableoid;
	PGPROC	   *wi_proc;
	TimestampTz wi_launchtime;
	bool		wi_dobalance;
	bool		wi_sharedrel;
	double		wi_cost_delay;
	int			wi_cost_limit;
	int			wi_cost_limit_base;
} WorkerInfoData;

typedef struct WorkerInfoData *WorkerInfo;

/*
 * 启动器从远程进程接收的可能信号。这些信号
 * 原子地存储在共享内存中，以便其他进程可以在
 * 不锁定的情况下设置它们。
 */
typedef enum
{
	AutoVacForkFailed,			/* 尝试启动工作者失败 */
	AutoVacRebalance,			/* 重新平衡成本限制 */
	AutoVacNumSignals			/* 必须是最后一个 */
}			AutoVacuumSignal;

/*
 * Autovacuum 工作项数组，存储在 AutoVacuumShmem->av_workItems 中。这个
 * 列表主要受 AutovacuumLock 保护，除了如果某个项目标记为 "active"
 * 则其他进程不得修改工作识别字段。
 */
typedef struct AutoVacuumWorkItem
{
	AutoVacuumWorkItemType avw_type;
	bool		avw_used;		/* 以下数据是有效的 */
	bool		avw_active;		/* 正在处理 */
	Oid			avw_database;
	Oid			avw_relation;
	BlockNumber avw_blockNumber;
} AutoVacuumWorkItem;

#define NUM_WORKITEMS	256

/*-------------
 * 主要的 autovacuum 共享内存结构。在共享内存中，我们存储这个主要
 * 结构及 WorkerInfo 结构的数组。该结构保持：
 *
 * av_signal		由其他进程设置以指示各种条件
 * av_launcherpid	autovacuum 启动器的 PID
 * av_freeWorkers	WorkerInfo 空闲列表
 * av_runningWorkers WorkerInfo 非空闲队列
 * av_startingWorker 指向当前正在启动的 WorkerInfo 的指针（由
 *					工作者自己在其运行后立即清除）
 * av_workItems		工作项数组
 *
 * 此结构由 AutovacuumLock 保护，除了 av_signal 和工作者列表的部分
 *（见上文）。
 *-------------
 */
typedef struct
{
	sig_atomic_t av_signal[AutoVacNumSignals];
	pid_t		av_launcherpid;
	dlist_head	av_freeWorkers;
	dlist_head	av_runningWorkers;
	WorkerInfo	av_startingWorker;
	AutoVacuumWorkItem av_workItems[NUM_WORKITEMS];
} AutoVacuumShmemStruct;

static AutoVacuumShmemStruct *AutoVacuumShmem;

/*
 * 启动器中的数据库列表（avl_dbase 元素），以及包含它的上下文
 */
static dlist_head DatabaseList = DLIST_STATIC_INIT(DatabaseList);
static MemoryContext DatabaseListCxt = NULL;

/* 指向我自己的 WorkerInfo 的指针，在每个工作者中有效 */
static WorkerInfo MyWorkerInfo = NULL;

/* 启动器的 PID，仅在工作者关闭时有效 */
int			AutovacuumLauncherPid = 0;

#ifdef EXEC_BACKEND
static pid_t fc_avlauncher_forkexec(void);
static pid_t fc_avworker_forkexec(void);
#endif
NON_EXEC_STATIC void fc_AutoVacWorkerMain(int fc_argc, char *fc_argv[]) pg_attribute_noreturn();
NON_EXEC_STATIC void fc_AutoVacLauncherMain(int fc_argc, char *fc_argv[]) pg_attribute_noreturn();

static Oid	fc_do_start_worker(void);
static void fc_HandleAutoVacLauncherInterrupts(void);
static void fc_AutoVacLauncherShutdown(void) pg_attribute_noreturn();
static void fc_launcher_determine_sleep(bool fc_canlaunch, bool fc_recursing,
									 struct timeval *fc_nap);
static void fc_launch_worker(TimestampTz fc_now);
static List *get_database_list(void);
static void fc_rebuild_database_list(Oid fc_newdb);
static int	fc_db_comparator(const void *fc_a, const void *fc_b);
static void fc_autovac_balance_cost(void);

static void fc_do_autovacuum(void);
static void fc_FreeWorkerInfo(int fc_code, Datum fc_arg);

static autovac_table *fc_table_recheck_autovac(Oid fc_relid, HTAB *fc_table_toast_map,
											TupleDesc pg_class_desc,
											int fc_effective_multixact_freeze_max_age);
static void fc_recheck_relation_needs_vacanalyze(Oid fc_relid, AutoVacOpts *fc_avopts,
											  Form_pg_class fc_classForm,
											  int fc_effective_multixact_freeze_max_age,
											  bool *fc_dovacuum, bool *fc_doanalyze, bool *fc_wraparound);
static void fc_relation_needs_vacanalyze(Oid fc_relid, AutoVacOpts *fc_relopts,
									  Form_pg_class fc_classForm,
									  PgStat_StatTabEntry *fc_tabentry,
									  int fc_effective_multixact_freeze_max_age,
									  bool *fc_dovacuum, bool *fc_doanalyze, bool *fc_wraparound);

static void fc_autovacuum_do_vac_analyze(autovac_table *fc_tab,
									  BufferAccessStrategy fc_bstrategy);
static AutoVacOpts *fc_extract_autovac_opts(HeapTuple fc_tup,
										 TupleDesc pg_class_desc);
static void fc_perform_work_item(AutoVacuumWorkItem *fc_workitem);
static void fc_autovac_report_activity(autovac_table *fc_tab);
static void fc_autovac_report_workitem(AutoVacuumWorkItem *fc_workitem,
									const char *fc_nspname, const char *fc_relname);
static void fc_avl_sigusr2_handler(SIGNAL_ARGS);



/********************************************************************
 *					  AUTOVACUUM 启动器代码
 ********************************************************************/

#ifdef EXEC_BACKEND
/*
 * autovacuum 启动器进程的 forkexec 例程。
 *
 * 格式化参数列表，然后进行 fork 和 exec。
 */
static pid_t fc_avlauncher_forkexec(void)
{
	char	   *fc_av[10];
	int			fc_ac = 0;

	fc_av[fc_ac++] = "postgres";
	fc_av[fc_ac++] = "--forkavlauncher";
	fc_av[fc_ac++] = NULL;			/* 由 postmaster_forkexec 填充 */
	fc_av[fc_ac] = NULL;

	Assert(fc_ac < lengthof(fc_av));

	return postmaster_forkexec(fc_ac, fc_av);
}

/*
 * 在调用 InitProcess 之前，我们需要从外部设置这个
 */
void AutovacuumLauncherIAm(void)
{
	am_autovacuum_launcher = true;
}
#endif

/*
 * autovacuum 启动器进程的主要入口点，从
 * postmaster 被调用。
 */
int StartAutoVacLauncher(void)
{
	pid_t		fc_AutoVacPID;

#ifdef EXEC_BACKEND
	switch ((fc_AutoVacPID = fc_avlauncher_forkexec()))
#else
	switch ((fc_AutoVacPID = fork_process()))
#endif
	{
		case -1:
			ereport(LOG,
					(errmsg("could not fork autovacuum launcher process: %m")));
			return 0;

#ifndef EXEC_BACKEND
		case 0:
			/* 在 postmaster 子进程中... */
			InitPostmasterChild();

			/* 关闭 postmaster 的套接字 */
			ClosePostmasterPorts(false);

			fc_AutoVacLauncherMain(0, NULL);
			break;
#endif
		default:
			return (int) fc_AutoVacPID;
	}

	/* 不应该到这里 */
	return 0;
}

/*
 * autovacuum 启动器进程的主循环。
 */
NON_EXEC_STATIC void fc_AutoVacLauncherMain(int fc_argc, char *fc_argv[])
{
	sigjmp_buf	fc_local_sigjmp_buf;

	am_autovacuum_launcher = true;

	MyBackendType = B_AUTOVAC_LAUNCHER;
	init_ps_display(NULL);

	ereport(DEBUG1,
			(errmsg_internal("autovacuum launcher started")));

	if (PostAuthDelay)
		pg_usleep(PostAuthDelay * 1000000L);

	SetProcessingMode(InitProcessing);

	/*
	 * 设置信号处理程序。我们在数据库上操作方式类似于常规
	 * 后端，所以我们使用相同的信号处理。请参见 tcop/postgres.c 中的等效代码。
	 */
	pqsignal(SIGHUP, SignalHandlerForConfigReload);
	pqsignal(SIGINT, StatementCancelHandler);
	pqsignal(SIGTERM, SignalHandlerForShutdownRequest);
	/* SIGQUIT 处理程序已经由 InitPostmasterChild 设置 */

	InitializeTimeouts();		/* 建立 SIGALRM 处理程序 */

	pqsignal(SIGPIPE, SIG_IGN);
	pqsignal(SIGUSR1, procsignal_sigusr1_handler);
	pqsignal(SIGUSR2, fc_avl_sigusr2_handler);
	pqsignal(SIGFPE, FloatExceptionHandler);
	pqsignal(SIGCHLD, SIG_DFL);

	/*
	 * 在共享内存中创建每个后端的 PGPROC 结构，EXEC_BACKEND 案例中
	 * 除外，此时在 SubPostmasterMain 中已经完成此操作。在我们可以使用
	 * LWLocks 之前必须执行此操作（在 EXEC_BACKEND 案例中，我们已经
	 * 必须处理 LWLocks）。
	 */
#ifndef EXEC_BACKEND
	InitProcess();
#endif

	/* 早期初始化 */
	BaseInit();

	InitPostgres(NULL, InvalidOid, NULL, InvalidOid, false, false, NULL);

	SetProcessingMode(NormalProcessing);

	/*
	 * 创建一个内存上下文，我们将在其中完成所有工作。我们这样做是为了
	 * 在错误恢复期间重置上下文，从而避免可能的内存泄漏。
	 */
	AutovacMemCxt = AllocSetContextCreate(TopMemoryContext,
										  "Autovacuum Launcher",
										  ALLOCSET_DEFAULT_SIZES);
	MemoryContextSwitchTo(AutovacMemCxt);

	/*
	 * 如果遇到异常，处理将在这里恢复。
	 *
	 * 这段代码是 PostgresMain 错误恢复的精简版。
	 *
	 * 请注意，我们使用 sigsetjmp(..., 1)，这样普遍的信号屏蔽
	 * （即 BlockSig）将在 longjmp 到这里时恢复。因此，除了
	 * SIGQUIT 之外的信号将在完成错误恢复之前被屏蔽。看起来这个策略
	 * 使得 HOLD_INTERRUPTS() 的调用是多余的，但并不是因为 InterruptPending
	 * 可能已经设置。
	 */
	if (sigsetjmp(fc_local_sigjmp_buf, 1) != 0)
	{
		/* 由于未使用 PG_TRY，必须手动重置错误堆栈 */
		error_context_stack = NULL;

		/* 清理时防止中断 */
		HOLD_INTERRUPTS();

		/* 忘记任何待处理的 QueryCancel 或超时请求 */
		disable_all_timeouts(false);
		QueryCancelPending = false; /* 第二次以避免竞争条件 */

		/* 将错误报告到服务器日志 */
		EmitErrorReport();

		/* 中止当前事务以进行恢复 */
		AbortCurrentTransaction();

		/*
		 * 释放其他资源，以防我们不在事务中。
		 */
		LWLockReleaseAll();
		pgstat_report_wait_end();
		AbortBufferIO();
		UnlockBuffers();
		/* 这可能是死代码，但我们还是要安全起见： */
		if (AuxProcessResourceOwner)
			ReleaseAuxProcessResources(false);
		AtEOXact_Buffers(false);
		AtEOXact_SMgr();
		AtEOXact_Files(false);
		AtEOXact_HashTables(false);

		/*
		 * 现在返回到正常的顶级上下文并清除 ErrorContext，以便下次使用。
		 */
		MemoryContextSwitchTo(AutovacMemCxt);
		FlushErrorState();

		/* 刷新顶级上下文中任何泄漏的数据 */
		MemoryContextResetAndDeleteChildren(AutovacMemCxt);

		/* 不要留下指向已释放内存的悬空指针 */
		DatabaseListCxt = NULL;
		dlist_init(&DatabaseList);

		/* 现在我们可以再次允许中断 */
		RESUME_INTERRUPTS();

		/* 如果处于关闭模式，则不需要进一步操作；就此离开 */
		if (ShutdownRequestPending)
			fc_AutoVacLauncherShutdown();

		/*
		 * 在任何错误后至少睡眠 1 秒。我们不希望以最快的速度填充错误日志。
		 */
		pg_usleep(1000000L);
	}

	/* 我们现在可以处理 ereport(ERROR) */
	PG_exception_stack = &fc_local_sigjmp_buf;

	/* 在调用 rebuild_database_list 之前必须解除信号阻塞 */
	PG_SETMASK(&UnBlockSig);

	/*
	 * 设置始终安全的搜索路径。启动器不连接到数据库，
	 * 因此这没有影响。
	 */
	SetConfigOption("search_path", "", PGC_SUSET, PGC_S_OVERRIDE);

	/*
	 * 强制在 autovac 进程中关闭 zero_damaged_pages，即使它在 postgresql.conf 中被设置。我们实际上并不希望如此危险的选项以非交互方式应用。
	 */
	SetConfigOption("zero_damaged_pages", "false", PGC_SUSET, PGC_S_OVERRIDE);

	/*
	 * 强制关闭可设置超时，以避免这些设置阻止定期维护的执行。
	 */
	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
	SetConfigOption("idle_in_transaction_session_timeout", "0",
					PGC_SUSET, PGC_S_OVERRIDE);

	/*
	 * 强制 default_transaction_isolation 为 READ COMMITTED。我们不希望承担可序列化模式的开销，也不希望增加导致死锁或延迟其他事务的风险。
	 */
	SetConfigOption("default_transaction_isolation", "read committed",
					PGC_SUSET, PGC_S_OVERRIDE);

	/*
	 * 即使系统配置为使用不同的获取一致性，
	 * 对于 autovac，我们始终想要新鲜的统计信息。
	 */
	SetConfigOption("stats_fetch_consistency", "none", PGC_SUSET, PGC_S_OVERRIDE);

	/*
	 * 在紧急模式下，启动一个工作程序（除非请求关闭）
	 * 然后离开。
	 */
	if (!AutoVacuumingActive())
	{
		if (!ShutdownRequestPending)
			fc_do_start_worker();
		proc_exit(0);			/* 完成 */
	}

	AutoVacuumShmem->av_launcherpid = MyProcPid;

	/*
	 * 创建初始数据库列表。我们希望这个列表保持的恒量是
	 * 按 decreasing next_time 排序。只要有条目
	 * 更新为更高的时间，它将被移动到前面（这是
	 * 正确的，因为唯一的操作是将 autovacuum_naptime 添加到条目中，时间总是增加）。
	 */
	fc_rebuild_database_list(InvalidOid);

	/* 循环直到请求关闭 */
	while (!ShutdownRequestPending)
	{
		struct timeval fc_nap;
		TimestampTz fc_current_time = 0;
		bool		fc_can_launch;

		/*
		 * 这个循环与 WaitLatch 的正常使用有一点不同，
		 * 因为我们希望在第一个子进程启动之前休眠。
		 * 所以是 WaitLatch，然后 ResetLatch，然后检查
		 * 醒来条件。
		 */

		fc_launcher_determine_sleep(!dlist_is_empty(&AutoVacuumShmem->av_freeWorkers),
								 false, &fc_nap);

		/*
		 * 等待直到 naptime 到期或我们收到某种信号（所有
		 * 信号处理程序通过调用 SetLatch 唤醒我们）。
		 */
		(void) WaitLatch(MyLatch,
						 WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
						 (fc_nap.tv_sec * 1000L) + (fc_nap.tv_usec / 1000L),
						 WAIT_EVENT_AUTOVACUUM_MAIN);

		ResetLatch(MyLatch);

		fc_HandleAutoVacLauncherInterrupts();

		/*
		 * 工作程序完成，或 postmaster 发出失败信号以启动工作程序
		 */
		if (got_SIGUSR2)
		{
			got_SIGUSR2 = false;

			/* 如果需要，重新平衡成本限制 */
			if (AutoVacuumShmem->av_signal[AutoVacRebalance])
			{
				LWLockAcquire(AutovacuumLock, LW_EXCLUSIVE);
				AutoVacuumShmem->av_signal[AutoVacRebalance] = false;
				fc_autovac_balance_cost();
				LWLockRelease(AutovacuumLock);
			}

			if (AutoVacuumShmem->av_signal[AutoVacForkFailed])
			{
				/*
				 * 如果 postmaster 无法启动新的工作程序，我们将休眠
				 * 一段时间并重新发送信号。新工作程序的
				 * 状态仍在内存中，因此这就足够了。之后，
				 * 我们重新启动主循环。
				 *
				 * XXX 我们是否应该限制重试的次数？
				 * 我认为这没有太大意义，因为未来启动
				 * 工作程序将继续以相同的方式失败。
				 */
				AutoVacuumShmem->av_signal[AutoVacForkFailed] = false;
				pg_usleep(1000000L);	/* 1s */
				SendPostmasterSignal(PMSIGNAL_START_AUTOVAC_WORKER);
				continue;
			}
		}

		/*
		 * 在尝试启动工作程序之前，我们需要检查一些条件。
		 * 首先，我们需要确保有可用的工作程序槽。其次，我们需要确保
		 * 在启动时没有其他工作程序失败。
		 */

		fc_current_time = GetCurrentTimestamp();
		LWLockAcquire(AutovacuumLock, LW_SHARED);

		fc_can_launch = !dlist_is_empty(&AutoVacuumShmem->av_freeWorkers);

		if (AutoVacuumShmem->av_startingWorker != NULL)
		{
			int			fc_waittime;
			WorkerInfo	fc_worker = AutoVacuumShmem->av_startingWorker;

			/*
			 * 当另一个工作程序仍在启动中（或在此过程中失败）时，
			 * 我们无法启动另一个工作程序，因此只需再
			 * 睡一会儿；那个工作程序会在准备好后再次唤醒我们。
			 * 不过，我们最多只会等待 autovacuum_naptime 秒（上限为
			 * 60 秒）以等待这一事件发生。注意，无法连接到特定数据库
			 * 在这里不是问题，因为工作程序在尝试连接之前
			 * 将自己从 startingWorker 指针中移除。postmaster 检测到的问题
			 * （如 fork() 失败）也会以不同方式报告和处理。唯有在
			 * AutoVacWorkerMain 的早期部分检测到的错误可能会
			 * 导致这段代码触发，是在工作程序将 WorkerInfo
			 * 从 startingWorker 指针中移除之前。
			 */
			fc_waittime = Min(autovacuum_naptime, 60) * 1000;
			if (TimestampDifferenceExceeds(fc_worker->wi_launchtime, fc_current_time,
										   fc_waittime))
			{
				LWLockRelease(AutovacuumLock);
				LWLockAcquire(AutovacuumLock, LW_EXCLUSIVE);

				/*
				 * 其他进程不能将工作程序置于启动模式，因此如果
				 * 在交换锁后 startingWorker 仍然无效，
				 * 我们就假设它是我们上述看到的同一个（因此我们不
				 * 重新检查启动时间）。
				 */
				if (AutoVacuumShmem->av_startingWorker != NULL)
				{
					fc_worker = AutoVacuumShmem->av_startingWorker;
					fc_worker->wi_dboid = InvalidOid;
					fc_worker->wi_tableoid = InvalidOid;
					fc_worker->wi_sharedrel = false;
					fc_worker->wi_proc = NULL;
					fc_worker->wi_launchtime = 0;
					dlist_push_head(&AutoVacuumShmem->av_freeWorkers,
									&fc_worker->wi_links);
					AutoVacuumShmem->av_startingWorker = NULL;
					ereport(WARNING,
							errmsg("autovacuum worker took too long to start; canceled"));
				}
			}
			else
				fc_can_launch = false;
		}
		LWLockRelease(AutovacuumLock);	/* 共享或独占 */

		/* 如果我们无能为力，就继续睡觉 */
		if (!fc_can_launch)
			continue;

		/* 我们可以开始新的工作程序 */

		if (dlist_is_empty(&DatabaseList))
		{
			/*
			 * 特殊情况，当列表为空时：立即启动一个工作程序。
			 * 这涵盖了初始情况，即没有数据库在 pgstats 中
			 * （因此列表为空）。注意 launcher_determine_sleep 中的约束
			 * 防止我们太快启动工作程序（空列表时最多每
			 * autovacuum_naptime 启动一次）。
			 */
			fc_launch_worker(fc_current_time);
		}
		else
		{
			/*
			 * 因为 rebuild_database_list 构建的列表中最远的 adl_next_worker 在前，所以我们从列表的尾部获取数据库。
			 */
			avl_dbase  *fc_avdb;

			fc_avdb = dlist_tail_element(avl_dbase, adl_node, &DatabaseList);

			/*
			 * 如果 next_worker 正在进行中或在过去则启动一个工作者。
			 */
			if (TimestampDifferenceExceeds(fc_avdb->adl_next_worker,
										   fc_current_time, 0))
				fc_launch_worker(fc_current_time);
		}
	}

	fc_AutoVacLauncherShutdown();
}

/*
 * 处理任何新的中断。
 */
static void fc_HandleAutoVacLauncherInterrupts(void)
{
	/* 正常关闭情况 */
	if (ShutdownRequestPending)
		fc_AutoVacLauncherShutdown();

	if (ConfigReloadPending)
	{
		ConfigReloadPending = false;
		ProcessConfigFile(PGC_SIGHUP);

		/* 配置文件中请求关闭？ */
		if (!AutoVacuumingActive())
			fc_AutoVacLauncherShutdown();

		/* 在默认成本参数更改的情况下进行重平衡 */
		LWLockAcquire(AutovacuumLock, LW_EXCLUSIVE);
		fc_autovac_balance_cost();
		LWLockRelease(AutovacuumLock);

		/* 在 naptime 改变的情况下重建列表 */
		fc_rebuild_database_list(InvalidOid);
	}

	/* 处理屏障事件 */
	if (ProcSignalBarrierPending)
		ProcessProcSignalBarrier();

	/* 执行此进程的内存上下文日志记录 */
	if (LogMemoryContextPending)
		ProcessLogMemoryContextInterrupt();

	/* 处理在休眠时发生的 sinval 赶超中断 */
	ProcessCatchupInterrupt();
}

/*
 * 从 autovac 启动器正常退出。
 */
static void fc_AutoVacLauncherShutdown(void)
{
	ereport(DEBUG1,
			(errmsg_internal("autovacuum launcher shutting down")));
	AutoVacuumShmem->av_launcherpid = 0;

	proc_exit(0);				/* 完成 */
}

/*
 * 根据数据库列表确定睡眠时间。
 *
 * “canlaunch”参数指示我们是否可以立即启动工作者，例如由于工作者都忙。如果这是假，则会导致较长的休眠，直到工作者退出时被打断。
 */
static void fc_launcher_determine_sleep(bool fc_canlaunch, bool fc_recursing, struct timeval *fc_nap)
{
	/*
	 * 我们会睡到下一个预定的 vacuum。我们相信当构建数据库列表时，已经注意到没有条目有过去的时间；如果第一个条目的 next_worker 值太近，或者时间在过去，我们将睡一个较短的名义时间。
	 */
	if (!fc_canlaunch)
	{
		fc_nap->tv_sec = autovacuum_naptime;
		fc_nap->tv_usec = 0;
	}
	else if (!dlist_is_empty(&DatabaseList))
	{
		TimestampTz fc_current_time = GetCurrentTimestamp();
		TimestampTz fc_next_wakeup;
		avl_dbase  *fc_avdb;
		long		fc_secs;
		int			fc_usecs;

		fc_avdb = dlist_tail_element(avl_dbase, adl_node, &DatabaseList);

		fc_next_wakeup = fc_avdb->adl_next_worker;
		TimestampDifference(fc_current_time, fc_next_wakeup, &fc_secs, &fc_usecs);

		fc_nap->tv_sec = fc_secs;
		fc_nap->tv_usec = fc_usecs;
	}
	else
	{
		/* 列表为空，整个 autovacuum_naptime 秒内休眠 */
		fc_nap->tv_sec = autovacuum_naptime;
		fc_nap->tv_usec = 0;
	}

	/*
	 * 如果结果恰好为零，则表示数据库有一个过去时间的条目。重建列表以使数据库重新均匀分布，并重新计算睡眠时间。这种情况可能发生在需要 vacuum 的表比工作者多，并且它们的 vacuum 时间都超过了 autovacuum_naptime。
	 *
	 * 我们只递归一次。rebuild_database_list 应始终返回将来的时间，但最好不要过于信任这个。
	 */
	if (fc_nap->tv_sec == 0 && fc_nap->tv_usec == 0 && !fc_recursing)
	{
		fc_rebuild_database_list(InvalidOid);
		fc_launcher_determine_sleep(fc_canlaunch, true, fc_nap);
		return;
	}

	/* 启动器允许休眠的最小时间。 */
	if (fc_nap->tv_sec <= 0 && fc_nap->tv_usec <= MIN_AUTOVAC_SLEEPTIME * 1000)
	{
		fc_nap->tv_sec = 0;
		fc_nap->tv_usec = MIN_AUTOVAC_SLEEPTIME * 1000;
	}

	/*
	 * 如果睡眠时间过长，将其限制为任意最大值（加上任何分秒，为了简单）。这避免了在系统时钟往回走几年等奇怪情况下几乎无限的睡眠。
	 */
	if (fc_nap->tv_sec > MAX_AUTOVAC_SLEEPTIME)
		fc_nap->tv_sec = MAX_AUTOVAC_SLEEPTIME;
}

/*
 * 构建更新的 DatabaseList。它必须只包含出现在 pgstats 中的数据库，并且必须按 next_worker 从高到低排序，均匀分布在下一个 autovacuum_naptime 间隔内。
 *
 * 接收生成此列表的数据库的 Oid（我们称之为“新”数据库，因为当数据库已在列表上时，我们期待该函数根本不被调用）。如果有，现有列表将用于保持数据库在 autovacuum_naptime 期间的顺序。新数据库将放在间隔的末尾。实际值不会被保存，这应该不会造成太大问题。
 */
static void fc_rebuild_database_list(Oid fc_newdb)
{
	List	   *fc_dblist;
	ListCell   *fc_cell;
	MemoryContext fc_newcxt;
	MemoryContext fc_oldcxt;
	MemoryContext fc_tmpcxt;
	HASHCTL		fc_hctl;
	int			fc_score;
	int			fc_nelems;
	HTAB	   *fc_dbhash;
	dlist_iter	fc_iter;

	fc_newcxt = AllocSetContextCreate(AutovacMemCxt,
								   "Autovacuum database list",
								   ALLOCSET_DEFAULT_SIZES);
	fc_tmpcxt = AllocSetContextCreate(fc_newcxt,
								   "Autovacuum database list (tmp)",
								   ALLOCSET_DEFAULT_SIZES);
	fc_oldcxt = MemoryContextSwitchTo(fc_tmpcxt);

	/*
	 * 实现这个并不像听起来那么简单，因为我们需要将新数据库放在列表末尾；接下来是已在列表上的数据库，最后（在列表的尾部）是所有未在现有列表上的其他数据库。
	 *
	 * 为此，我们构建一个空的已评分数据库的哈希表。我们将从新数据库的最低分（零）开始，接着按顺序为现有列表中的数据库增加分数，最后为所有通过 get_database_list() 获取的而不在哈希表中的数据库增加分数。
	 *
	 * 然后，我们将所有哈希元素放入一个数组，按分数对数组进行排序，最后将数组元素放入新的双向链表中。
	 */
	fc_hctl.keysize = sizeof(Oid);
	fc_hctl.entrysize = sizeof(avl_dbase);
	fc_hctl.hcxt = fc_tmpcxt;
	fc_dbhash = hash_create("autovacuum db hash", 20, &fc_hctl,	/* 这里的魔法数字
															 * FIXME */
						 HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);

	/* 开始插入新数据库 */
	fc_score = 0;
	if (OidIsValid(fc_newdb))
	{
		avl_dbase  *fc_db;
		PgStat_StatDBEntry *fc_entry;

		/* 仅考虑此数据库，如果它有 pgstat 条目 */
		fc_entry = pgstat_fetch_stat_dbentry(fc_newdb);
		if (fc_entry != NULL)
		{
			/* 我们假设未找到，因为哈希是刚创建的 */
			fc_db = hash_search(fc_dbhash, &fc_newdb, HASH_ENTER, NULL);

			/* hash_search 已经填充了键 */
			fc_db->adl_score = fc_score++;
			/* next_worker 会在后面填充 */
		}
	}

	/* 现在插入现有列表中的数据库 */
	dlist_foreach(fc_iter, &DatabaseList)
	{
		avl_dbase  *fc_avdb = dlist_container(avl_dbase, adl_node, fc_iter.cur);
		avl_dbase  *fc_db;
		bool		fc_found;
		PgStat_StatDBEntry *fc_entry;

		/*
		 * 跳过没有 stat 条目的数据库 —— 特别是，这将去掉已删除的数据库
		 */
		fc_entry = pgstat_fetch_stat_dbentry(fc_avdb->adl_datid);
		if (fc_entry == NULL)
			continue;

		fc_db = hash_search(fc_dbhash, &(fc_avdb->adl_datid), HASH_ENTER, &fc_found);

		if (!fc_found)
		{
			/* hash_search 已经填充了键 */
			fc_db->adl_score = fc_score++;
			/* next_worker 会在后面填充 */
		}
	}

	/* 最后，插入所有未之前插入的合格数据库 */
	fc_dblist = get_database_list();
	foreach(fc_cell, fc_dblist)
	{
		avw_dbase  *fc_avdb = lfirst(fc_cell);
		avl_dbase  *fc_db;
		bool		fc_found;
		PgStat_StatDBEntry *fc_entry;

		/* 仅考虑具有 pgstat 条目的数据库 */
		fc_entry = pgstat_fetch_stat_dbentry(fc_avdb->adw_datid);
		if (fc_entry == NULL)
			continue;

		fc_db = hash_search(fc_dbhash, &(fc_avdb->adw_datid), HASH_ENTER, &fc_found);
		/* 仅在数据库未在哈希上时更新分数 */
		if (!fc_found)
		{
			/* hash_search 已经填充了键 */
			fc_db->adl_score = fc_score++;
			/* next_worker 会在后面填充 */
		}
	}
	fc_nelems = fc_score;

	/* 从这里开始，分配的内存属于新列表 */
	MemoryContextSwitchTo(fc_newcxt);
	dlist_init(&DatabaseList);

	if (fc_nelems > 0)
	{
		TimestampTz fc_current_time;
		int			fc_millis_increment;
		avl_dbase  *fc_dbary;
		avl_dbase  *fc_db;
		HASH_SEQ_STATUS fc_seq;
		int			fc_i;

		/* 将所有哈希元素放入数组中 */
		fc_dbary = palloc(fc_nelems * sizeof(avl_dbase));

		fc_i = 0;
		hash_seq_init(&fc_seq, fc_dbhash);
		while ((fc_db = hash_seq_search(&fc_seq)) != NULL)
			memcpy(&(fc_dbary[fc_i++]), fc_db, sizeof(avl_dbase));

		/* 对数组进行排序 */
		qsort(fc_dbary, fc_nelems, sizeof(avl_dbase), fc_db_comparator);

		/*
		 * 确定计划中数据库之间的时间间隔。如果
		 * 我们发现配置的休眠时间会使我们进入
		 * 低于最小休眠时间（launcher_determine_sleep
		 * 代码中不允许的情况），则默默地使用更长的休眠时间
		 * （但不触碰 GUC 变量）。
		 */
		fc_millis_increment = 1000.0 * autovacuum_naptime / fc_nelems;
		if (fc_millis_increment <= MIN_AUTOVAC_SLEEPTIME)
			fc_millis_increment = MIN_AUTOVAC_SLEEPTIME * 1.1;

		fc_current_time = GetCurrentTimestamp();

		/*
		 * 将元素从数组移动到 dlist，同时在遍历数组时设置
		 * next_worker
		 */
		for (fc_i = 0; fc_i < fc_nelems; fc_i++)
		{
			avl_dbase  *fc_db = &(fc_dbary[fc_i]);

			fc_current_time = TimestampTzPlusMilliseconds(fc_current_time,
													   fc_millis_increment);
			fc_db->adl_next_worker = fc_current_time;

			/* 後面的元素应更靠近列表的头部 */
			dlist_push_head(&DatabaseList, &fc_db->adl_node);
		}
	}

	/* 完成所有操作，清理内存 */
	if (DatabaseListCxt != NULL)
		MemoryContextDelete(DatabaseListCxt);
	MemoryContextDelete(fc_tmpcxt);
	DatabaseListCxt = fc_newcxt;
	MemoryContextSwitchTo(fc_oldcxt);
}

/* avl_dbase 的 qsort 比较函数，使用 adl_score */
static int fc_db_comparator(const void *fc_a, const void *fc_b)
{
	if (((const avl_dbase *) fc_a)->adl_score == ((const avl_dbase *) fc_b)->adl_score)
		return 0;
	else
		return (((const avl_dbase *) fc_a)->adl_score < ((const avl_dbase *) fc_b)->adl_score) ? 1 : -1;
}

/*
 * do_start_worker
 *
 * 从启动器启动自动清理工作者的基本步骤。
 * 它确定要处理哪个数据库，设置共享内存并
 * 向主进程发送信号以启动工作者。如果在
 * autovacuum_workers 已经激活时被调用，则会优雅失败。
 *
 * 返回值是工作者将要处理的数据库的 OID，
 * 如果没有实际启动工作者，则返回 InvalidOid。
 */
static Oid fc_do_start_worker(void)
{
	List	   *fc_dblist;
	ListCell   *fc_cell;
	TransactionId fc_xidForceLimit;
	MultiXactId fc_multiForceLimit;
	bool		fc_for_xid_wrap;
	bool		fc_for_multi_wrap;
	avw_dbase  *fc_avdb;
	TimestampTz fc_current_time;
	bool		fc_skipit = false;
	Oid			fc_retval = InvalidOid;
	MemoryContext fc_tmpcxt,
				fc_oldcxt;

	/* 当没有空闲工作者时快速返回 */
	LWLockAcquire(AutovacuumLock, LW_SHARED);
	if (dlist_is_empty(&AutoVacuumShmem->av_freeWorkers))
	{
		LWLockRelease(AutovacuumLock);
		return InvalidOid;
	}
	LWLockRelease(AutovacuumLock);

	/*
	 * 创建并切换到临时上下文以避免泄漏
	 * 为数据库列表分配的内存。
	 */
	fc_tmpcxt = AllocSetContextCreate(CurrentMemoryContext,
								   "Autovacuum start worker (tmp)",
								   ALLOCSET_DEFAULT_SIZES);
	fc_oldcxt = MemoryContextSwitchTo(fc_tmpcxt);

	/* 获取数据库列表 */
	fc_dblist = get_database_list();

	/*
	 * 确定我们允许通过而不强制清理的最古老的
	 * datfrozenxid/relfrozenxid。(此限制可以针对
	 * 特定表收紧，但不能放宽。)
	 */
	recentXid = ReadNextTransactionId();
	fc_xidForceLimit = recentXid - autovacuum_freeze_max_age;
	/* 确保这是一个“正常”的 XID，否则 TransactionIdPrecedes 将行为异常 */
	/* 这可能会导致限制向后移动 3，但这没关系 */
	if (fc_xidForceLimit < FirstNormalTransactionId)
		fc_xidForceLimit -= FirstNormalTransactionId;

	/* 还要确定我们将考虑的最古老的 datminmxid。 */
	recentMulti = ReadNextMultiXactId();
	fc_multiForceLimit = recentMulti - MultiXactMemberFreezeThreshold();
	if (fc_multiForceLimit < FirstMultiXactId)
		fc_multiForceLimit -= FirstMultiXactId;

	/*
	 * 选择要连接的数据库。我们选择最近最少
	 * 进行自动清理的数据库，或者需要清理以防止 Xid
	 * 环绕相关的数据丢失。如果发现任何有 Xid 环绕风险的数据库，
	 * 我们选择最古老的 datfrozenxid，无视
	 * 自动清理时间；类似地，如果有在 MultiXactId 环绕中，
	 * 我们选择最古老的 datminmxid。注意，处于 Xid 环绕
	 * 危险中的优先于处于多重环绕危险中的。
	 *
	 * 注意，通常不考虑没有 stats 条目的数据库，除了
	 * Xid 环绕的目的。理论上，如果自从最后初始化 stats 以来
	 * 没有人连接过它，就不需要清理。
	 *
	 * XXX 如果在连接之前我们有更多信息关于它是否需要
	 * 清理，这可以改进。也许查看数据库表的 pgstats 数据？
	 * 一个想法是跟踪每个数据库在 pgstats 中的新元组和死元组的数量。
	 * 不过，目前尚不清楚如何构建一个度量来衡量这一点，并且
	 * 不会导致不太繁忙的数据库饿死。
	 */
	fc_avdb = NULL;
	fc_for_xid_wrap = false;
	fc_for_multi_wrap = false;
	fc_current_time = GetCurrentTimestamp();
	foreach(fc_cell, fc_dblist)
	{
		avw_dbase  *fc_tmp = lfirst(fc_cell);
		dlist_iter	fc_iter;

		/* 检查这个数据库是否有环绕的风险 */
		if (TransactionIdPrecedes(fc_tmp->adw_frozenxid, fc_xidForceLimit))
		{
			if (fc_avdb == NULL ||
				TransactionIdPrecedes(fc_tmp->adw_frozenxid,
									  fc_avdb->adw_frozenxid))
				fc_avdb = fc_tmp;
			fc_for_xid_wrap = true;
			continue;
		}
		else if (fc_for_xid_wrap)
			continue;			/* 忽略没有风险的数据库 */
		else if (MultiXactIdPrecedes(fc_tmp->adw_minmulti, fc_multiForceLimit))
		{
			if (fc_avdb == NULL ||
				MultiXactIdPrecedes(fc_tmp->adw_minmulti, fc_avdb->adw_minmulti))
				fc_avdb = fc_tmp;
			fc_for_multi_wrap = true;
			continue;
		}
		else if (fc_for_multi_wrap)
			continue;			/* 忽略没有风险的数据库 */

		/* 查找 pgstat 条目（如果有） */
		fc_tmp->adw_entry = pgstat_fetch_stat_dbentry(fc_tmp->adw_datid);

		/*
		 * 跳过没有 pgstat 条目的数据库；这意味着它没有看到任何
		 * 活动。
		 */
		if (!fc_tmp->adw_entry)
			continue;

		/*
		 * 此外，跳过在数据库列表中显示为最近
		 * 被处理的数据库（少于 autovacuum_naptime 秒前）。
		 * 我们这样做是为了避免选择一个我们刚刚选择的数据库，而那个 pgstat
		 * 还没有更新最后的自动清理时间。
		 */
		fc_skipit = false;

		dlist_reverse_foreach(fc_iter, &DatabaseList)
		{
			avl_dbase  *fc_dbp = dlist_container(avl_dbase, adl_node, fc_iter.cur);

			if (fc_dbp->adl_datid == fc_tmp->adw_datid)
			{
				/*
				 * 如果这个数据库的 next_worker 值在
				 * 当前时间和当前时间加上 naptime 之间，则跳过这个数据库。
				 */
				if (!TimestampDifferenceExceeds(fc_dbp->adl_next_worker,
												fc_current_time, 0) &&
					!TimestampDifferenceExceeds(fc_current_time,
												fc_dbp->adl_next_worker,
												autovacuum_naptime * 1000))
					fc_skipit = true;

				break;
			}
		}
		if (fc_skipit)
			continue;

		/*
		 * 记住最近的自动清理时间最早的数据库。（如果我们在这里，tmp->entry 和 db->entry 都必须非空。）
		 */
		if (fc_avdb == NULL ||
			fc_tmp->adw_entry->last_autovac_time < fc_avdb->adw_entry->last_autovac_time)
			fc_avdb = fc_tmp;
	}

	/* 找到一个数据库——对其进行处理 */
	if (fc_avdb != NULL)
	{
		WorkerInfo	fc_worker;
		dlist_node *fc_wptr;

		LWLockAcquire(AutovacuumLock, LW_EXCLUSIVE);

		/*
		 * 从自由列表中获取一个工作者条目。我们在上面检查过，因此
		 * 确实应该有一个空位。
		 */
		fc_wptr = dlist_pop_head_node(&AutoVacuumShmem->av_freeWorkers);

		fc_worker = dlist_container(WorkerInfoData, wi_links, fc_wptr);
		fc_worker->wi_dboid = fc_avdb->adw_datid;
		fc_worker->wi_proc = NULL;
		fc_worker->wi_launchtime = GetCurrentTimestamp();

		AutoVacuumShmem->av_startingWorker = fc_worker;

		LWLockRelease(AutovacuumLock);

		SendPostmasterSignal(PMSIGNAL_START_AUTOVAC_WORKER);

		fc_retval = fc_avdb->adw_datid;
	}
	else if (fc_skipit)
	{
		/*
		 * 如果我们跳过所有列表中的数据库，重建它，因为它
		 * 可能包含一个已删除的数据库。
		 */
		fc_rebuild_database_list(InvalidOid);
	}

	MemoryContextSwitchTo(fc_oldcxt);
	MemoryContextDelete(fc_tmpcxt);

	return fc_retval;
}

/*
 * launch_worker
 *
 * 从启动器启动工作者的封装。除了真正启动
 * 它，更新数据库列表以反映下次将在选定数据库上
 * 启动另一个工作者的时间。实际数据库选择
 * 交给 do_start_worker 完成。
 *
 * 此例程还预期在数据库列表中插入条目，
 * 如果选定的数据库之前缺失。
 */
static void fc_launch_worker(TimestampTz fc_now)
{
	Oid			fc_dbid;
	dlist_iter	fc_iter;

	fc_dbid = fc_do_start_worker();
	if (OidIsValid(fc_dbid))
	{
		bool		fc_found = false;

		/*
		 * 遍历数据库列表并更新相应的条目。如果数据库不在列表中，
		 * 我们将重新创建列表。
		 */
		dlist_foreach(fc_iter, &DatabaseList)
		{
			avl_dbase  *fc_avdb = dlist_container(avl_dbase, adl_node, fc_iter.cur);

			if (fc_avdb->adl_datid == fc_dbid)
			{
				fc_found = true;

				/*
				 * 将 autovacuum_naptime 秒添加到当前时间，然后将其用作
				 * 此数据库的新“next_worker”字段。
				 */
				fc_avdb->adl_next_worker =
					TimestampTzPlusMilliseconds(fc_now, autovacuum_naptime * 1000);

				dlist_move_head(&DatabaseList, fc_iter.cur);
				break;
			}
		}

		
		if (!fc_found)
			fc_rebuild_database_list(fc_dbid);
	}
}

/*
 * 从主进程调用以指示无法分叉一个进程成为
 * 工作进程。主进程应在调用此函数后不久发送kill(SIGUSR2)信号给启动器。
 */
void AutoVacWorkerFailed(void)
{
	AutoVacuumShmem->av_signal[AutoVacForkFailed] = true;
}

/* SIGUSR2: 工作进程已启动并运行，或刚刚完成，或未能分叉 */
static void fc_avl_sigusr2_handler(SIGNAL_ARGS)
{
	int			fc_save_errno = errno;

	got_SIGUSR2 = true;
	SetLatch(MyLatch);

	errno = fc_save_errno;
}


/********************************************************************
 *					  自动清理工作进程代码
 ********************************************************************/

#ifdef EXEC_BACKEND
/*
 * 自动清理工作进程的分叉执行例程。
 *
 * 格式化参数列表，然后分叉和执行。
 */
static pid_t fc_avworker_forkexec(void)
{
	char	   *fc_av[10];
	int			fc_ac = 0;

	fc_av[fc_ac++] = "postgres";
	fc_av[fc_ac++] = "--forkavworker";
	fc_av[fc_ac++] = NULL;			/* 由 postmaster_forkexec 填充 */
	fc_av[fc_ac] = NULL;

	Assert(fc_ac < lengthof(fc_av));

	return postmaster_forkexec(fc_ac, fc_av);
}

/*
 * 在调用 InitProcess 之前，我们需要从外部设置这个
 */
void AutovacuumWorkerIAm(void)
{
	am_autovacuum_worker = true;
}
#endif

/*
 * 自动清理工作进程的主要入口点。
 *
 * 此代码主要基于pgarch.c，请参考。
 */
int StartAutoVacWorker(void)
{
	pid_t		fc_worker_pid;

#ifdef EXEC_BACKEND
	switch ((fc_worker_pid = fc_avworker_forkexec()))
#else
	switch ((fc_worker_pid = fork_process()))
#endif
	{
		case -1:
			ereport(LOG,
					(errmsg("could not fork autovacuum worker process: %m")));
			return 0;

#ifndef EXEC_BACKEND
		case 0:
			/* 在 postmaster 子进程中... */
			InitPostmasterChild();

			/* 关闭 postmaster 的套接字 */
			ClosePostmasterPorts(false);

			fc_AutoVacWorkerMain(0, NULL);
			break;
#endif
		default:
			return (int) fc_worker_pid;
	}

	/* 不应该到这里 */
	return 0;
}

/*
 * AutoVacWorkerMain
 */
NON_EXEC_STATIC void fc_AutoVacWorkerMain(int fc_argc, char *fc_argv[])
{
	sigjmp_buf	fc_local_sigjmp_buf;
	Oid			fc_dbid;

	am_autovacuum_worker = true;

	MyBackendType = B_AUTOVAC_WORKER;
	init_ps_display(NULL);

	SetProcessingMode(InitProcessing);

	/*
	 * 设置信号处理程序。我们在数据库上操作方式类似于常规
	 * 后端，所以我们使用相同的信号处理。请参见 tcop/postgres.c 中的等效代码。
	 */
	pqsignal(SIGHUP, SignalHandlerForConfigReload);

	/*
	 * SIGINT用于信号取消当前表的清理；SIGTERM
	 * 意味着中止并干净退出，而SIGQUIT意味着放弃任务。
	 */
	pqsignal(SIGINT, StatementCancelHandler);
	pqsignal(SIGTERM, die);
	/* SIGQUIT 处理程序已经由 InitPostmasterChild 设置 */

	InitializeTimeouts();		/* 建立 SIGALRM 处理程序 */

	pqsignal(SIGPIPE, SIG_IGN);
	pqsignal(SIGUSR1, procsignal_sigusr1_handler);
	pqsignal(SIGUSR2, SIG_IGN);
	pqsignal(SIGFPE, FloatExceptionHandler);
	pqsignal(SIGCHLD, SIG_DFL);

	/*
	 * 在共享内存中创建每个后端的 PGPROC 结构，EXEC_BACKEND 案例中
	 * 除外，此时在 SubPostmasterMain 中已经完成此操作。在我们可以使用
	 * LWLocks 之前必须执行此操作（在 EXEC_BACKEND 案例中，我们已经
	 * 必须处理 LWLocks）。
	 */
#ifndef EXEC_BACKEND
	InitProcess();
#endif

	/* 早期初始化 */
	BaseInit();

	/*
	 * 如果遇到异常，处理从这里恢复。
	 *
	 * 与大多数辅助进程不同，我们不尝试在发生错误后继续
	 * 处理；我们只是清理并退出。自动清理
	 * 启动器负责稍后生成另一个工作进程。
	 *
	 * 请注意，我们使用sigsetjmp(..., 1)，这样，当前的信号屏蔽
	 * （即BlockSig）将在长跳转到这里时恢复。因此，
	 * 其他信号除了SIGQUIT都将被阻塞，直到我们退出。这可能
	 * 使得HOLD_INTERRUPTS()调用显得多余，但并非如此，因为
	 * InterruptPending可能已经被设置。
	 */
	if (sigsetjmp(fc_local_sigjmp_buf, 1) != 0)
	{
		/* 由于未使用 PG_TRY，必须手动重置错误堆栈 */
		error_context_stack = NULL;

		/* 清理时防止中断 */
		HOLD_INTERRUPTS();

		/* 将错误报告到服务器日志 */
		EmitErrorReport();

		/*
		 * 我们现在可以离开了。请注意，由于我们调用了InitProcess，
		 * 一个回调已被注册以执行ProcKill，这将清理
		 * 必要的状态。
		 */
		proc_exit(0);
	}

	/* 我们现在可以处理 ereport(ERROR) */
	PG_exception_stack = &fc_local_sigjmp_buf;

	PG_SETMASK(&UnBlockSig);

	/*
	 * 设置始终安全的搜索路径，以便恶意用户无法重定向用户
	 * 代码（例如pg_index.indexprs）。（该代码在
	 * SECURITY_RESTRICTED_OPERATION沙箱中运行，因此恶意用户无论如何无法
	 * 控制整个自动清理工作进程。）
	 */
	SetConfigOption("search_path", "", PGC_SUSET, PGC_S_OVERRIDE);

	/*
	 * 强制在 autovac 进程中关闭 zero_damaged_pages，即使它在 postgresql.conf 中被设置。我们实际上并不希望如此危险的选项以非交互方式应用。
	 */
	SetConfigOption("zero_damaged_pages", "false", PGC_SUSET, PGC_S_OVERRIDE);

	/*
	 * 强制关闭可设置超时，以避免这些设置阻止定期维护的执行。
	 */
	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
	SetConfigOption("idle_in_transaction_session_timeout", "0",
					PGC_SUSET, PGC_S_OVERRIDE);

	/*
	 * 强制 default_transaction_isolation 为 READ COMMITTED。我们不希望承担可序列化模式的开销，也不希望增加导致死锁或延迟其他事务的风险。
	 */
	SetConfigOption("default_transaction_isolation", "read committed",
					PGC_SUSET, PGC_S_OVERRIDE);

	/*
	 * 强制关闭同步复制以允许常规维护，即使
	 * 我们在等待备用连接。这一点很重要，以确保我们
	 * 不会被阻止执行反包围任务。
	 */
	if (synchronous_commit > SYNCHRONOUS_COMMIT_LOCAL_FLUSH)
		SetConfigOption("synchronous_commit", "local",
						PGC_SUSET, PGC_S_OVERRIDE);

	/*
	 * 即使系统配置为使用不同的获取一致性，
	 * 对于 autovac，我们始终想要新鲜的统计信息。
	 */
	SetConfigOption("stats_fetch_consistency", "none", PGC_SUSET, PGC_S_OVERRIDE);

	/*
	 * 获取我们将要处理的数据库的信息。
	 */
	LWLockAcquire(AutovacuumLock, LW_EXCLUSIVE);

	/*
	 * 当心startingWorker为INVALID；这通常不
	 * 应该发生，但如果一个工作进程在分叉后而在此之前失败，
	 * 启动器可能已决定将其从队列中移除并重新开始。
	 */
	if (AutoVacuumShmem->av_startingWorker != NULL)
	{
		MyWorkerInfo = AutoVacuumShmem->av_startingWorker;
		fc_dbid = MyWorkerInfo->wi_dboid;
		MyWorkerInfo->wi_proc = MyProc;

		/* 插入到运行列表中 */
		dlist_push_head(&AutoVacuumShmem->av_runningWorkers,
						&MyWorkerInfo->wi_links);

		/*
		 * 从“启动”指针中移除，以便启动器可以在需要时
		 * 启动一个新工作进程
		 */
		AutoVacuumShmem->av_startingWorker = NULL;
		LWLockRelease(AutovacuumLock);

		on_shmem_exit(fc_FreeWorkerInfo, 0);

		/* 唤醒启动器 */
		if (AutoVacuumShmem->av_launcherpid != 0)
			kill(AutoVacuumShmem->av_launcherpid, SIGUSR2);
	}
	else
	{
		/* 我没有工作进程条目，走开 */
		elog(WARNING, "autovacuum worker started without a worker entry");
		fc_dbid = InvalidOid;
		LWLockRelease(AutovacuumLock);
	}

	if (OidIsValid(fc_dbid))
	{
		char		fc_dbname[NAMEDATALEN];

		/*
		 * 将自动清理启动报告给累积统计系统。我们
		 * 故意在InitPostgres之前执行此操作，以便
		 * last_autovac_time即使在连接尝试
		 * 失败时也会被更新。这是为了防止自动清理被“卡住”，反复
		 * 选择一个无法打开的数据库，而不是在可以连接的东西上取得进展。
		 */
		pgstat_report_autovac(fc_dbid);

		/*
		 * 连接到选定的数据库，不指定特定用户，
		 * 并忽略datallowconn。收集数据库的名称以便
		 * 显示。
		 *
		 * 注意：如果我们选择了一个刚刚删除的数据库（由于使用
		 * 过时的统计信息），我们将在这里失败并退出。
		 */
		InitPostgres(NULL, fc_dbid, NULL, InvalidOid, false, true,
					 fc_dbname);
		SetProcessingMode(NormalProcessing);
		set_ps_display(fc_dbname);
		ereport(DEBUG1,
				(errmsg_internal("autovacuum: processing database \"%s\"", fc_dbname)));

		if (PostAuthDelay)
			pg_usleep(PostAuthDelay * 1000000L);

		/* 做适量的工作 */
		recentXid = ReadNextTransactionId();
		recentMulti = ReadNextMultiXactId();
		fc_do_autovacuum();
	}

	/*
	 * 启动器将在ProcKill中被通知我的死亡，*如果*我们成功
	 * 获取到了一个工作进程插槽
	 */

	/* 全部完成，走开 */
	proc_exit(0);
}

/*
 * 将WorkerInfo返回到自由列表
 */
static void fc_FreeWorkerInfo(int fc_code, Datum fc_arg)
{
	if (MyWorkerInfo != NULL)
	{
		LWLockAcquire(AutovacuumLock, LW_EXCLUSIVE);

		/*
		 * 唤醒启动器，以便在需要时立即启动新的工作进程。
		 * 我们仅在本地内存中保存启动器的PID；
		 * 实际信号将在PGPROC被回收时发送。请注意
		 * 我们始终这样做，以便启动器可以重新平衡
		 * 剩余工作进程的成本限制设置。
		 *
		 * 我们在某种程度上忽略了启动器在我们读取PID和实际杀死之间改变其PID的风险；
		 * 我们期望ProcKill会在我们之后不久被调用，并且我们假设PIDs在进程退出后不会被
		 * 太快地重新使用。
		 */
		AutovacuumLauncherPid = AutoVacuumShmem->av_launcherpid;

		dlist_delete(&MyWorkerInfo->wi_links);
		MyWorkerInfo->wi_dboid = InvalidOid;
		MyWorkerInfo->wi_tableoid = InvalidOid;
		MyWorkerInfo->wi_sharedrel = false;
		MyWorkerInfo->wi_proc = NULL;
		MyWorkerInfo->wi_launchtime = 0;
		MyWorkerInfo->wi_dobalance = false;
		MyWorkerInfo->wi_cost_delay = 0;
		MyWorkerInfo->wi_cost_limit = 0;
		MyWorkerInfo->wi_cost_limit_base = 0;
		dlist_push_head(&AutoVacuumShmem->av_freeWorkers,
						&MyWorkerInfo->wi_links);
		/* 不再是我的工作 */
		MyWorkerInfo = NULL;

		/*
		 * 现在我们不活跃，导致存活的
		 * 工作进程重新平衡
		 */
		AutoVacuumShmem->av_signal[AutoVacRebalance] = true;
		LWLockRelease(AutovacuumLock);
	}
}

/*
 * 更新基于成本的延迟参数，以便多个工作进程各自消耗
 * 总可用I/O的一个部分。
 */
void AutoVacuumUpdateDelay(void)
{
	if (MyWorkerInfo)
	{
		VacuumCostDelay = MyWorkerInfo->wi_cost_delay;
		VacuumCostLimit = MyWorkerInfo->wi_cost_limit;
	}
}

/*
 * autovac_balance_cost
 *		重新计算每个活动工作进程的成本限制设置。
 *
 * 调用者必须以独占模式持有AutovacuumLock。
 */
static void fc_autovac_balance_cost(void)
{
	
/*
	 * 这里的想法是我们均等地分配 I/O。一个工作者可以消耗的 I/O 
	 * 量是由 cost_limit/cost_delay 决定的，因此我们尝试平衡这些比率，而不是原始的限制设置。
	 *
	 * 注意：在 cost_limit 中，零也意味着使用其他地方的值，因为
	 * 零不是一个有效值。
	 */
	int			fc_vac_cost_limit = (autovacuum_vac_cost_limit > 0 ?
								  autovacuum_vac_cost_limit : VacuumCostLimit);
	double		fc_vac_cost_delay = (autovacuum_vac_cost_delay >= 0 ?
								  autovacuum_vac_cost_delay : VacuumCostDelay);
	double		fc_cost_total;
	double		fc_cost_avail;
	dlist_iter	fc_iter;

	/* 未设置？没什么好做的 */
	if (fc_vac_cost_limit <= 0 || fc_vac_cost_delay <= 0)
		return;

	/* 计算参与的活跃工作者的总基础成本限制 */
	fc_cost_total = 0.0;
	dlist_foreach(fc_iter, &AutoVacuumShmem->av_runningWorkers)
	{
		WorkerInfo	fc_worker = dlist_container(WorkerInfoData, wi_links, fc_iter.cur);

		if (fc_worker->wi_proc != NULL &&
			fc_worker->wi_dobalance &&
			fc_worker->wi_cost_limit_base > 0 && fc_worker->wi_cost_delay > 0)
			fc_cost_total +=
				(double) fc_worker->wi_cost_limit_base / fc_worker->wi_cost_delay;
	}

	/* 没有成本限制 -- 无需操作 */
	if (fc_cost_total <= 0)
		return;

	/*
	 * 调整每个活跃工作者的成本限制，以平衡总成本
	 * 限制到 autovacuum_vacuum_cost_limit。
	 */
	fc_cost_avail = (double) fc_vac_cost_limit / fc_vac_cost_delay;
	dlist_foreach(fc_iter, &AutoVacuumShmem->av_runningWorkers)
	{
		WorkerInfo	fc_worker = dlist_container(WorkerInfoData, wi_links, fc_iter.cur);

		if (fc_worker->wi_proc != NULL &&
			fc_worker->wi_dobalance &&
			fc_worker->wi_cost_limit_base > 0 && fc_worker->wi_cost_delay > 0)
		{
			int			fc_limit = (int)
			(fc_cost_avail * fc_worker->wi_cost_limit_base / fc_cost_total);

			/*
			 * 我们将成本限制的下限设定为1，以避免在
			 * vacuum 代码中的零除错误。此外，为了避免
			 * 在这些计算中出现四舍五入问题，确保
			 * 不会把成本限制设置得超过基础值。
			 */
			fc_worker->wi_cost_limit = Max(Min(fc_limit,
											fc_worker->wi_cost_limit_base),
										1);
		}

		if (fc_worker->wi_proc != NULL)
			elog(DEBUG2, "autovac_balance_cost(pid=%d db=%u, rel=%u, dobalance=%s cost_limit=%d, cost_limit_base=%d, cost_delay=%g)",
				 fc_worker->wi_proc->pid, fc_worker->wi_dboid, fc_worker->wi_tableoid,
				 fc_worker->wi_dobalance ? "yes" : "no",
				 fc_worker->wi_cost_limit, fc_worker->wi_cost_limit_base,
				 fc_worker->wi_cost_delay);
	}
}

/*
 * get_database_list
 *		返回在 pg_database 中找到的所有数据库的列表。
 *
 * 列表和关联数据分配在调用者的内存上下文中，
 * 负责确保之后正确清理。
 *
 * 注意：这是 autovacuum 启动器使用事务的唯一函数。
 * 虽然我们没有连接到任何特定数据库，因此无法访问大多数目录，
 * 但我们有足够的基础设施在 pg_database 上进行 seqscan。
 */
static List * get_database_list(void)
{
	List	   *fc_dblist = NIL;
	Relation	fc_rel;
	TableScanDesc fc_scan;
	HeapTuple	fc_tup;
	MemoryContext fc_resultcxt;

	/* 这是我们将在其中分配输出数据的上下文 */
	fc_resultcxt = CurrentMemoryContext;

	/*
	 * 开始一个事务，以便我们可以访问 pg_database，并获取快照。
	 * 我们并不需要快照本身，但我们对它设置的 RecentGlobalXmin
	 * 的副作用很感兴趣。（这对读取堆页面的任何操作都是关键的，
	 * 因为 HOT 可能决定修剪它们，即使这个过程并没有试图修改任何元组。）
	 *
	 * FIXME: 这个评论不准确/代码有bug。一个未被推送/激活的快照
	 * 并不能可靠地防止 HOT 修剪（-> xmin 可能会在处理缓存无效时被清除）。
	 */
	StartTransactionCommand();
	(void) GetTransactionSnapshot();

	fc_rel = table_open(DatabaseRelationId, AccessShareLock);
	fc_scan = table_beginscan_catalog(fc_rel, 0, NULL);

	while (HeapTupleIsValid(fc_tup = heap_getnext(fc_scan, ForwardScanDirection)))
	{
		Form_pg_database fc_pgdatabase = (Form_pg_database) GETSTRUCT(fc_tup);
		avw_dbase  *fc_avdb;
		MemoryContext fc_oldcxt;

		/*
		 * 如果数据库部分被删除，我们不能，也不需要，
		 * 对其进行 vacuum。
		 */
		if (database_is_invalid_form(fc_pgdatabase))
		{
			elog(DEBUG2,
				 "autovacuum: skipping invalid database \"%s\"",
				 NameStr(fc_pgdatabase->datname));
			continue;
		}

		/*
		 * 在调用者的上下文中分配我们的结果，而不是在
		 * 事务中。我们在循环内部执行此操作，并在最后恢复原始
		 * 上下文，以便像 heap_getnext() 这样的泄露操作不会
		 * 在潜在的长期上下文中被调用。
		 */
		fc_oldcxt = MemoryContextSwitchTo(fc_resultcxt);

		fc_avdb = (avw_dbase *) palloc(sizeof(avw_dbase));

		fc_avdb->adw_datid = fc_pgdatabase->oid;
		fc_avdb->adw_name = pstrdup(NameStr(fc_pgdatabase->datname));
		fc_avdb->adw_frozenxid = fc_pgdatabase->datfrozenxid;
		fc_avdb->adw_minmulti = fc_pgdatabase->datminmxid;
		/* 这个稍后会设置： */
		fc_avdb->adw_entry = NULL;

		fc_dblist = lappend(fc_dblist, fc_avdb);
		MemoryContextSwitchTo(fc_oldcxt);
	}

	table_endscan(fc_scan);
	table_close(fc_rel, AccessShareLock);

	CommitTransactionCommand();

	/* 确保恢复调用者的内存上下文 */
	MemoryContextSwitchTo(fc_resultcxt);

	return fc_dblist;
}

/*
 * 按数据库表逐个处理
 *
 * 请注意，CHECK_FOR_INTERRUPTS 应在某些位置使用，
 * 以免忽视关闭命令太长时间。
 */
static void fc_do_autovacuum(void)
{
	Relation	fc_classRel;
	HeapTuple	fc_tuple;
	TableScanDesc fc_relScan;
	Form_pg_database fc_dbForm;
	List	   *fc_table_oids = NIL;
	List	   *fc_orphan_oids = NIL;
	HASHCTL		fc_ctl;
	HTAB	   *fc_table_toast_map;
	ListCell   *volatile fc_cell;
	BufferAccessStrategy fc_bstrategy;
	ScanKeyData fc_key;
	TupleDesc	pg_class_desc;
	int			fc_effective_multixact_freeze_max_age;
	bool		fc_did_vacuum = false;
	bool		fc_found_concurrent_worker = false;
	int			fc_i;

	/*
	 * StartTransactionCommand 和 CommitTransactionCommand 将自动
	 * 切换到其他上下文。我们需要这个上下文来保留
	 * 跨事务的关系列表，以便能进行 vacuum/analyze。
	 */
	AutovacMemCxt = AllocSetContextCreate(TopMemoryContext,
										  "Autovacuum worker",
										  ALLOCSET_DEFAULT_SIZES);
	MemoryContextSwitchTo(AutovacMemCxt);

	/* 开始一个事务，以便我们的命令可以进入。 */
	StartTransactionCommand();

	/*
	 * 计算冻结紧急的 multixact 年龄。这通常是 autovacuum_multixact_freeze_max_age，但如果我们缺少 multixact 成员空间，则可能会更少。
	 */
	fc_effective_multixact_freeze_max_age = MultiXactMemberFreezeThreshold();

	/*
	 * 找到 pg_database 条目并选择默认的冻结年龄。我们在模板和
	 * 无法连接的数据库中使用零，否则使用系统范围的默认值。
	 */
	fc_tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
	if (!HeapTupleIsValid(fc_tuple))
		elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
	fc_dbForm = (Form_pg_database) GETSTRUCT(fc_tuple);

	if (fc_dbForm->datistemplate || !fc_dbForm->datallowconn)
	{
		default_freeze_min_age = 0;
		default_freeze_table_age = 0;
		default_multixact_freeze_min_age = 0;
		default_multixact_freeze_table_age = 0;
	}
	else
	{
		default_freeze_min_age = vacuum_freeze_min_age;
		default_freeze_table_age = vacuum_freeze_table_age;
		default_multixact_freeze_min_age = vacuum_multixact_freeze_min_age;
		default_multixact_freeze_table_age = vacuum_multixact_freeze_table_age;
	}

	ReleaseSysCache(fc_tuple);

	/* StartTransactionCommand 在其他地方更改 */
	MemoryContextSwitchTo(AutovacMemCxt);

	fc_classRel = table_open(RelationRelationId, AccessShareLock);

	/* 创建一个副本，以便在关闭 pg_class 后使用 */
	pg_class_desc = CreateTupleDescCopy(RelationGetDescr(fc_classRel));

	/* 创建 hash 表以进行 toast <-> main relid 映射 */
	fc_ctl.keysize = sizeof(Oid);
	fc_ctl.entrysize = sizeof(av_relation);

	fc_table_toast_map = hash_create("TOAST to main relid map",
								  100,
								  &fc_ctl,
								  HASH_ELEM | HASH_BLOBS);

	/*
	 * 扫描 pg_class，以确定要 vacuum 的表。
	 *
	 * 我们分两遍进行：第一次收集普通关系和
	 * 物化视图的列表，第二次收集 TOAST 表。第二次遍历的原因是
	 * 在此期间，如果 TOAST 表没有任何条目，我们想使用主
	 * 关系的 pg_class.reloptions 条目，而在
	 * 预先不知道主表 OID 的情况下无法获取。
	 *
	 * 我们需要单独检查 TOAST 表，因为在短而宽的表
	 * 的情况下，可能在 TOAST 表上的活动与其父表相比
	 * 会相对多得多。
	 */
	fc_relScan = table_beginscan_catalog(fc_classRel, 0, NULL);

	/*
	 * 在第一次遍历中，我们收集要 vacuum 的主表，以及主
	 * 表 relid 到 TOAST relid 的映射。
	 */
	while ((fc_tuple = heap_getnext(fc_relScan, ForwardScanDirection)) != NULL)
	{
		Form_pg_class fc_classForm = (Form_pg_class) GETSTRUCT(fc_tuple);
		PgStat_StatTabEntry *fc_tabentry;
		AutoVacOpts *fc_relopts;
		Oid			fc_relid;
		bool		fc_dovacuum;
		bool		fc_doanalyze;
		bool		fc_wraparound;

		if (fc_classForm->relkind != RELKIND_RELATION &&
			fc_classForm->relkind != RELKIND_MATVIEW)
			continue;

		fc_relid = fc_classForm->oid;

		/*
		 * 检查是否为临时表（大概是其他后端的临时表）。
		 * 我们无法安全处理其他后端的临时表。
		 */
		if (fc_classForm->relpersistence == RELPERSISTENCE_TEMP)
		{
			/*
			 * 如果拥有的后端仍然处于活动状态并使用临时架构，
			 * 则我们直接忽略它。此外，为了安全起见，
			 * 如果命名空间不存在或不是临时命名空间，则也是
			 * 忽略它。
			 */
			if (checkTempNamespaceStatus(fc_classForm->relnamespace) == TEMP_NAMESPACE_IDLE)
			{
				/*
				 * 表似乎已经孤立 -- 尽管拥有的后端可能已
				 * 删除并退出；我们的 pg_class 扫描快照不一定是最新的，
				 * 因此我们可能看到了一个已提交的死条目。
				 * 我们记住它，以便稍后尝试删除。
				 */
				fc_orphan_oids = lappend_oid(fc_orphan_oids, fc_relid);
			}
			continue;
		}

		/* 获取此表的 reloptions 和 pgstat 条目 */
		fc_relopts = fc_extract_autovac_opts(fc_tuple, pg_class_desc);
		fc_tabentry = pgstat_fetch_stat_tabentry_ext(fc_classForm->relisshared,
												  fc_relid);

		/* 检查是否需要 vacuum 或分析 */
		fc_relation_needs_vacanalyze(fc_relid, fc_relopts, fc_classForm, fc_tabentry,
								  fc_effective_multixact_freeze_max_age,
								  &fc_dovacuum, &fc_doanalyze, &fc_wraparound);

		/* 需要处理的关系被添加到 table_oids */
		if (fc_dovacuum || fc_doanalyze)
			fc_table_oids = lappend_oid(fc_table_oids, fc_relid);

		/*
		 * 记住 TOAST 关联以供第二遍使用。注意：无论该表是否
		 * 要进行 vacuum，我们都必须执行此操作，因为我们
		 * 不会自动对父表进行 vacuum 时处理 toast 表。
		 */
		if (OidIsValid(fc_classForm->reltoastrelid))
		{
			av_relation *fc_hentry;
			bool		fc_found;

			fc_hentry = hash_search(fc_table_toast_map,
								 &fc_classForm->reltoastrelid,
								 HASH_ENTER, &fc_found);

			if (!fc_found)
			{
				/* hash_search 已经填充了键 */
				fc_hentry->ar_relid = fc_relid;
				fc_hentry->ar_hasrelopts = false;
				if (fc_relopts != NULL)
				{
					fc_hentry->ar_hasrelopts = true;
					memcpy(&fc_hentry->ar_reloptions, fc_relopts,
						   sizeof(AutoVacOpts));
				}
			}
		}
	}

	table_endscan(fc_relScan);

	/* 第二遍：检查 TOAST 表 */
	ScanKeyInit(&fc_key,
				Anum_pg_class_relkind,
				BTEqualStrategyNumber, F_CHAREQ,
				CharGetDatum(RELKIND_TOASTVALUE));

	fc_relScan = table_beginscan_catalog(fc_classRel, 1, &fc_key);
	while ((fc_tuple = heap_getnext(fc_relScan, ForwardScanDirection)) != NULL)
	{
		Form_pg_class fc_classForm = (Form_pg_class) GETSTRUCT(fc_tuple);
		PgStat_StatTabEntry *fc_tabentry;
		Oid			fc_relid;
		AutoVacOpts *fc_relopts = NULL;
		bool		fc_dovacuum;
		bool		fc_doanalyze;
		bool		fc_wraparound;

		/*
		 * 我们无法安全处理其他后端的临时表，因此跳过它们。
		 */
		if (fc_classForm->relpersistence == RELPERSISTENCE_TEMP)
			continue;

		fc_relid = fc_classForm->oid;

		/*
		 * 获取 reloptions -- 如果这个 toast 表没有它们，则尝试主表
		 */
		fc_relopts = fc_extract_autovac_opts(fc_tuple, pg_class_desc);
		if (fc_relopts == NULL)
		{
			av_relation *fc_hentry;
			bool		fc_found;

			fc_hentry = hash_search(fc_table_toast_map, &fc_relid, HASH_FIND, &fc_found);
			if (fc_found && fc_hentry->ar_hasrelopts)
				fc_relopts = &fc_hentry->ar_reloptions;
		}

		/* 获取此表的 pgstat 条目 */
		fc_tabentry = pgstat_fetch_stat_tabentry_ext(fc_classForm->relisshared,
												  fc_relid);

		fc_relation_needs_vacanalyze(fc_relid, fc_relopts, fc_classForm, fc_tabentry,
								  fc_effective_multixact_freeze_max_age,
								  &fc_dovacuum, &fc_doanalyze, &fc_wraparound);

		/* 忽略 toast 表的分析 */
		if (fc_dovacuum)
			fc_table_oids = lappend_oid(fc_table_oids, fc_relid);
	}

	table_endscan(fc_relScan);
	table_close(fc_classRel, AccessShareLock);

	/*
	 * 重新检查孤儿临时表，如果它们仍然看起来是孤儿表，则删除
	 * 它们。每删除一个表我们将消耗一个事务，这看起来可能
	 * 过多，但我们只应该在先前后端崩溃的结果下需要做任何事情，
	 * 因此这不应该经常发生到值得“优化”。
	 * 使用单独的事务确保了如果有许多临时表被删除时，
	 * 我们不会使锁表膨胀，并确保如果删除尝试失败，
	 * 我们不会丢失工作。
	 */
	foreach(fc_cell, fc_orphan_oids)
	{
		Oid			fc_relid = lfirst_oid(fc_cell);
		Form_pg_class fc_classForm;
		ObjectAddress fc_object;

		/*
		 * 检查用户请求的中止。
		 */
		CHECK_FOR_INTERRUPTS();

		/*
		 * 尝试锁定表。如果我们无法立即获得锁，则说明其他人正在使用（或删除）该表，因此这不再是我们关心的内容。拥有锁可以防止下面的竞争条件。
		 */
		if (!ConditionalLockRelationOid(fc_relid, AccessExclusiveLock))
			continue;

		/*
		 * 重新获取 pg_class 元组并重新检查它是否仍然似乎是一个孤立的临时表。如果它不在那里或不再是相同的关系，则忽略它。
		 */
		fc_tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(fc_relid));
		if (!HeapTupleIsValid(fc_tuple))
		{
			/* 确保放弃无用的锁，以免导致锁表膨胀 */
			UnlockRelationOid(fc_relid, AccessExclusiveLock);
			continue;
		}
		fc_classForm = (Form_pg_class) GETSTRUCT(fc_tuple);

		/*
		 * 进行与上面的循环中相同的测试。如果 OID 计数器发生回绕，我们现在拥有的 pg_class 条目可能与我们之前看到的完全无关。
		 */
		if (!((fc_classForm->relkind == RELKIND_RELATION ||
			   fc_classForm->relkind == RELKIND_MATVIEW) &&
			  fc_classForm->relpersistence == RELPERSISTENCE_TEMP))
		{
			UnlockRelationOid(fc_relid, AccessExclusiveLock);
			continue;
		}

		if (checkTempNamespaceStatus(fc_classForm->relnamespace) != TEMP_NAMESPACE_IDLE)
		{
			UnlockRelationOid(fc_relid, AccessExclusiveLock);
			continue;
		}

		/*
		 * 尝试锁定临时命名空间。尽管我们已经锁定了表本身，但仍然存在相对于试图清除临时命名空间的传入后端的死锁风险，以防此表有依赖项（例如序列），后端的 performDeletion 调用可能会以不同的顺序访问这些依赖项。如果我们能在命名空间上获得 AccessShareLock，那就足以确保我们不会与 RemoveTempRelations 并发运行。如果不能，则后退并让 RemoveTempRelations 做它的事情。
		 */
		if (!ConditionalLockDatabaseObject(NamespaceRelationId,
										   fc_classForm->relnamespace, 0,
										   AccessShareLock))
		{
			UnlockRelationOid(fc_relid, AccessExclusiveLock);
			continue;
		}

		/* 好的，让我们删除它 */
		ereport(LOG,
				(errmsg("autovacuum: dropping orphan temp table \"%s.%s.%s\"",
						get_database_name(MyDatabaseId),
						get_namespace_name(fc_classForm->relnamespace),
						NameStr(fc_classForm->relname))));

		fc_object.classId = RelationRelationId;
		fc_object.objectId = fc_relid;
		fc_object.objectSubId = 0;
		performDeletion(&fc_object, DROP_CASCADE,
						PERFORM_DELETION_INTERNAL |
						PERFORM_DELETION_QUIETLY |
						PERFORM_DELETION_SKIP_EXTENSIONS);

		/*
		 * 为了提交删除，结束当前事务并开始一个新的事务。请注意，这也会释放我们所持的锁。
		 */
		CommitTransactionCommand();
		StartTransactionCommand();

		/* StartTransactionCommand 更改了当前的内存上下文 */
		MemoryContextSwitchTo(AutovacMemCxt);
	}

	/*
	 * 创建一个缓冲访问策略对象供 VACUUM 使用。我们希望在执行的所有 VACUUM 操作中使用同一个，因为 VACUUM 的目的是不破坏共享缓存。
	 */
	fc_bstrategy = GetAccessStrategy(BAS_VACUUM);

	/*
	 * 创建一个内存上下文来充当虚假的 PortalContext，以便在每个表的 VACUUM 代码中创建的上下文能被清理。
	 */
	PortalContext = AllocSetContextCreate(AutovacMemCxt,
										  "Autovacuum Portal",
										  ALLOCSET_DEFAULT_SIZES);

	/*
	 * 对收集的表执行操作。
	 */
	foreach(fc_cell, fc_table_oids)
	{
		Oid			fc_relid = lfirst_oid(fc_cell);
		HeapTuple	fc_classTup;
		autovac_table *fc_tab;
		bool		fc_isshared;
		bool		fc_skipit;
		double		fc_stdVacuumCostDelay;
		int			fc_stdVacuumCostLimit;
		dlist_iter	fc_iter;

		CHECK_FOR_INTERRUPTS();

		/*
		 * 在处理每个收集的表之前检查配置更改。
		 */
		if (ConfigReloadPending)
		{
			ConfigReloadPending = false;
			ProcessConfigFile(PGC_SIGHUP);

			/*
			 * 如果看到 autovacuum 现在已禁用，您可能会想退缩。必须抵制这种诱惑——这可能是一个用于回绕应急工作程序的情况，在这种情况下，这种行为完全不合适。
			 */
		}

		/*
		 * 找出该表是否为共享表。(仅仅为了这个而获取 syscache 条目稍微有些烦人，但是在典型情况下，因为 table_recheck_autovac 即使在这里重新获取条目，所以花费也很小。我们可以通过在这里复制元组并将其传递给 table_recheck_autovac 来弥补，但这增加了该函数与过时数据一起工作的可能性。)
		 */
		fc_classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(fc_relid));
		if (!HeapTupleIsValid(fc_classTup))
			continue;			/* 有人删除了关系，忘掉它 */
		fc_isshared = ((Form_pg_class) GETSTRUCT(fc_classTup))->relisshared;
		ReleaseSysCache(fc_classTup);

		/*
		 * 从这里开始保持调度锁，直到我们声称该表。我们还需要 AutovacuumLock 以遍历工作程序数组，但它可以仅是共享锁。
		 */
		LWLockAcquire(AutovacuumScheduleLock, LW_EXCLUSIVE);
		LWLockAcquire(AutovacuumLock, LW_SHARED);

		/*
		 * 检查该表是否正在被其他工作程序并发清理。
		 */
		fc_skipit = false;
		dlist_foreach(fc_iter, &AutoVacuumShmem->av_runningWorkers)
		{
			WorkerInfo	fc_worker = dlist_container(WorkerInfoData, wi_links, fc_iter.cur);

			/* 忽略我自己 */
			if (fc_worker == MyWorkerInfo)
				continue;

			/* 忽略其他数据库中的工作程序（除非表是共享的） */
			if (!fc_worker->wi_sharedrel && fc_worker->wi_dboid != MyDatabaseId)
				continue;

			if (fc_worker->wi_tableoid == fc_relid)
			{
				fc_skipit = true;
				fc_found_concurrent_worker = true;
				break;
			}
		}
		LWLockRelease(AutovacuumLock);
		if (fc_skipit)
		{
			LWLockRelease(AutovacuumScheduleLock);
			continue;
		}

		/*
		 * 在释放调度锁之前，将表的 OID 存储在共享内存中，以便其他工作程序不会试图并发清理它。（我们在这里声明它，以便在重新检查统计信息时不会保持 AutovacuumScheduleLock。）
		 */
		MyWorkerInfo->wi_tableoid = fc_relid;
		MyWorkerInfo->wi_sharedrel = fc_isshared;
		LWLockRelease(AutovacuumScheduleLock);

		/*
		 * 检查 pgstat 数据是否仍然表示我们需要清理该表。如果在我们不看的时候其他东西处理了该表，它可能已经改变。这并没有完全消除竞争条件，但它是非常小的。
		 */
		MemoryContextSwitchTo(AutovacMemCxt);
		fc_tab = fc_table_recheck_autovac(fc_relid, fc_table_toast_map, pg_class_desc,
									fc_effective_multixact_freeze_max_age);
		if (fc_tab == NULL)
		{
			/* 其他人已经清理了该表，或者它已经消失 */
			LWLockAcquire(AutovacuumScheduleLock, LW_EXCLUSIVE);
			MyWorkerInfo->wi_tableoid = InvalidOid;
			MyWorkerInfo->wi_sharedrel = false;
			LWLockRelease(AutovacuumScheduleLock);
			continue;
		}

		/*
		 * 记住 VACUUM 成本 GUC 的当前值。我们必须在循环底部恢复这些值，否则我们将在 autovac_balance_cost() 的下一个迭代中计算错误的值。
		 */
		fc_stdVacuumCostDelay = VacuumCostDelay;
		fc_stdVacuumCostLimit = VacuumCostLimit;

		/* 在处理成本平衡信息时必须保持 AutovacuumLock */
		LWLockAcquire(AutovacuumLock, LW_EXCLUSIVE);

		/* 为平衡算法宣传我的成本延迟参数 */
		MyWorkerInfo->wi_dobalance = fc_tab->at_dobalance;
		MyWorkerInfo->wi_cost_delay = fc_tab->at_vacuum_cost_delay;
		MyWorkerInfo->wi_cost_limit = fc_tab->at_vacuum_cost_limit;
		MyWorkerInfo->wi_cost_limit_base = fc_tab->at_vacuum_cost_limit;

		/* 进行平衡 */
		fc_autovac_balance_cost();

		/* 根据结果设置活动成本参数 */
		AutoVacuumUpdateDelay();

		/* 完成 */
		LWLockRelease(AutovacuumLock);

		/* 在每次迭代之前清理内存 */
		MemoryContextResetAndDeleteChildren(PortalContext);

		/*
		 * 保存关系名称以备可能的错误消息，以避免在发生错误时进行
		 * 目录查找。如果这些返回 NULL，则说明自上次检查以来
		 * 关系已被删除；跳过它。
		 * 注意：它们必须位于长期存活的内存上下文中，因为我们在
		 * 不同的事务中调用 vacuum 和 analyze。
		 */

		fc_tab->at_relname = get_rel_name(fc_tab->at_relid);
		fc_tab->at_nspname = get_namespace_name(get_rel_namespace(fc_tab->at_relid));
		fc_tab->at_datname = get_database_name(MyDatabaseId);
		if (!fc_tab->at_relname || !fc_tab->at_nspname || !fc_tab->at_datname)
			goto deleted;

		/*
		 * 如果出现错误，我们将中止当前表的清理，并继续下一个调度的表；特别地， 
		 * 如果我们被 SIGINT 中断，则会发生这种情况。
		 */
		PG_TRY();
		{
			/* 对于每个表的分配使用 PortalContext */
			MemoryContextSwitchTo(PortalContext);

			/* 开始吧 */
			fc_autovacuum_do_vac_analyze(fc_tab, fc_bstrategy);

			/*
			 * 清除可能的查询取消信号，以避免对由于清理当前表而自动发送的信号的延迟反应 
			 * （我们已经完成了，所以在这一点取消是没有意义的。）
			 */
			QueryCancelPending = false;
		}
		PG_CATCH();
		{
			/*
			 * 中止事务，启动一个新的事务，并继续处理我们列表中的下一个表。
			 */
			HOLD_INTERRUPTS();
			if (fc_tab->at_params.options & VACOPT_VACUUM)
				errcontext("automatic vacuum of table \"%s.%s.%s\"",
						   fc_tab->at_datname, fc_tab->at_nspname, fc_tab->at_relname);
			else
				errcontext("automatic analyze of table \"%s.%s.%s\"",
						   fc_tab->at_datname, fc_tab->at_nspname, fc_tab->at_relname);
			EmitErrorReport();

			/* 这也重置了 ProcGlobal->statusFlags[i] */
			AbortOutOfAnyTransaction();
			FlushErrorState();
			MemoryContextResetAndDeleteChildren(PortalContext);

			/* 重新启动我们的事务以进行后续操作 */
			StartTransactionCommand();
			RESUME_INTERRUPTS();
		}
		PG_END_TRY();

		/* 确保我们回到 AutovacMemCxt */
		MemoryContextSwitchTo(AutovacMemCxt);

		fc_did_vacuum = true;

		/* 在下一次 xact 结束时重置 ProcGlobal->statusFlags[i] */

		/* 保持整洁 */
deleted:
		if (fc_tab->at_datname != NULL)
			pfree(fc_tab->at_datname);
		if (fc_tab->at_nspname != NULL)
			pfree(fc_tab->at_nspname);
		if (fc_tab->at_relname != NULL)
			pfree(fc_tab->at_relname);
		pfree(fc_tab);

		/*
		 * 从共享内存中删除我的信息。我们可以清除 wi_cost_limit 等，但故意不做——
		 * 这是基于假设，我们可能还有更多事情要用相似的成本设置来处理，
		 * 所以我们不想在非常短的时间内放弃我们的 I/O 份额，从而导致全局平衡的混乱。
		 */
		LWLockAcquire(AutovacuumScheduleLock, LW_EXCLUSIVE);
		MyWorkerInfo->wi_tableoid = InvalidOid;
		MyWorkerInfo->wi_sharedrel = false;
		LWLockRelease(AutovacuumScheduleLock);

		/* 为下一次迭代恢复清理成本 GUCs */
		VacuumCostDelay = fc_stdVacuumCostDelay;
		VacuumCostLimit = fc_stdVacuumCostLimit;
	}

	/*
	 * 根据后端请求执行额外的工作项。
	 */
	LWLockAcquire(AutovacuumLock, LW_EXCLUSIVE);
	for (fc_i = 0; fc_i < NUM_WORKITEMS; fc_i++)
	{
		AutoVacuumWorkItem *fc_workitem = &AutoVacuumShmem->av_workItems[fc_i];

		if (!fc_workitem->avw_used)
			continue;
		if (fc_workitem->avw_active)
			continue;
		if (fc_workitem->avw_database != MyDatabaseId)
			continue;

		/* 声明这一点，并在执行时释放锁 */
		fc_workitem->avw_active = true;
		LWLockRelease(AutovacuumLock);

		fc_perform_work_item(fc_workitem);

		/*
		 * 在获取进一步工作的锁之前检查配置更改。
		 */
		CHECK_FOR_INTERRUPTS();
		if (ConfigReloadPending)
		{
			ConfigReloadPending = false;
			ProcessConfigFile(PGC_SIGHUP);
		}

		LWLockAcquire(AutovacuumLock, LW_EXCLUSIVE);

		/* 并标记其完成 */
		fc_workitem->avw_active = false;
		fc_workitem->avw_used = false;
	}
	LWLockRelease(AutovacuumLock);

	/*
	 * 我们在这里泄漏 table_toast_map（还有其他东西），但由于我们很快就要离开，这不是问题。
	 */

	/*
	 * 更新 pg_database.datfrozenxid，并在可能的情况下截断 pg_xact。我们
	 * 只需执行此操作一次，而不是在每个表后。
	 *
	 * 即使我们没有清理任何东西，这仍然可能很重要，因为 vac_update_datfrozenxid() 的一个间接效果
	 * 是更新 ShmemVariableCache->xidVacLimit。即使我们没有清理任何东西，也可能需要执行此操作，
	 * 因为具有较旧 relfrozenxid 值的关系或其他具有较旧 datfrozenxid 值的数据库可能已经被删除，
	 * 允许 xidVacLimit 前进。
	 *
	 * 但是，重要的是不要在所有情况下盲目执行此操作，因为当 autovacuum=off 时这将重新启动自动清理启动器。
	 * 如果我们不小心，可能会导致无限循环，其中工作者找不到工作并重新启动启动器，这将启动 
	 * 另一个在同一数据库中找到没有工作可做的工作者。为此，我们跳过
	 * 如果（1）我们没有找到工作可做并且（2）由于并发的自动清理活动我们跳过了至少一个表。
	 * 在这种情况下，另一个工作者已经完成了此操作，或者将在其完成时这样做。
	 */
	if (fc_did_vacuum || !fc_found_concurrent_worker)
		vac_update_datfrozenxid();

	/* 最后结束最后一个事务。 */
	CommitTransactionCommand();
}

/*
 * 执行先前注册的工作项。
 */
static void fc_perform_work_item(AutoVacuumWorkItem *fc_workitem)
{
	char	   *fc_cur_datname = NULL;
	char	   *fc_cur_nspname = NULL;
	char	   *fc_cur_relname = NULL;

	/*
	 * 请注意，我们不在 MyWorkerInfo 中存储表信息，因为这不是
	 * 正常的清理。
	 */

	/*
	 * 保存关系名称以备可能的错误消息，以避免在发生错误时进行目录查找。
	 * 如果其中任何一个返回 NULL，则表在我们最后检查后已被删除；跳过它。
	 */
	Assert(CurrentMemoryContext == AutovacMemCxt);

	fc_cur_relname = get_rel_name(fc_workitem->avw_relation);
	fc_cur_nspname = get_namespace_name(get_rel_namespace(fc_workitem->avw_relation));
	fc_cur_datname = get_database_name(MyDatabaseId);
	if (!fc_cur_relname || !fc_cur_nspname || !fc_cur_datname)
		goto deleted2;

	fc_autovac_report_workitem(fc_workitem, fc_cur_nspname, fc_cur_relname);

	/* 在每个工作项之前清理内存 */
	MemoryContextResetAndDeleteChildren(PortalContext);

	/*
	 * 如果出现错误，我们将中止当前工作项，并继续下一个；特别地，
	 * 如果我们被 SIGINT 中断，则会发生这种情况。请注意，这意味着工作项列表
	 * 可能会丢失。
	 */
	PG_TRY();
	{
		/* 对于每个工作项的分配使用 PortalContext */
		MemoryContextSwitchTo(PortalContext);

		/* 开始吧 */
		switch (fc_workitem->avw_type)
		{
			case AVW_BRINSummarizeRange:
				DirectFunctionCall2(brin_summarize_range,
									ObjectIdGetDatum(fc_workitem->avw_relation),
									Int64GetDatum((int64) fc_workitem->avw_blockNumber));
				break;
			default:
				elog(WARNING, "unrecognized work item found: type %d",
					 fc_workitem->avw_type);
				break;
		}

		/*
		 * 清除可能的查询取消信号，以避免对由于清理当前表而自动发送的信号的延迟反应 
		 * （我们已经完成了，所以在这一点上取消是没有意义的。）
		 */
		QueryCancelPending = false;
	}
	PG_CATCH();
	{
		/*
		 * 中止事务，启动一个新的事务，并继续处理我们列表中的下一个表。
		 */
		HOLD_INTERRUPTS();
		errcontext("processing work entry for relation \"%s.%s.%s\"",
				   fc_cur_datname, fc_cur_nspname, fc_cur_relname);
		EmitErrorReport();

		/* 这也重置了 ProcGlobal->statusFlags[i] */
		AbortOutOfAnyTransaction();
		FlushErrorState();
		MemoryContextResetAndDeleteChildren(PortalContext);

		/* 重新启动我们的事务以进行后续操作 */
		StartTransactionCommand();
		RESUME_INTERRUPTS();
	}
	PG_END_TRY();

	/* 确保我们回到 AutovacMemCxt */
	MemoryContextSwitchTo(AutovacMemCxt);

	/* 我们故意不在这里设置 did_vacuum */

	/* 保持整洁 */
deleted2:
	if (fc_cur_datname)
		pfree(fc_cur_datname);
	if (fc_cur_nspname)
		pfree(fc_cur_nspname);
	if (fc_cur_relname)
		pfree(fc_cur_relname);
}

/*
 * extract_autovac_opts
 *
 * 给定关系的 pg_class 元组，如果设置则返回 reloptions 的 AutoVacOpts 部分；
 * 否则，返回 NULL。
 *
 * 注意：调用者在这一点上没有在表上的关系锁，
 * 所以表可能已经被删除，并且它的目录行在我们获取 pg_class 行后消失了。
 * 如果 pg_class 有 TOAST 表，这将是一个风险；幸运的是，它没有。
 */
static AutoVacOpts *
fc_extract_autovac_opts(HeapTuple fc_tup, TupleDesc pg_class_desc)
{
	bytea	   *fc_relopts;
	AutoVacOpts *fc_av;

	Assert(((Form_pg_class) GETSTRUCT(fc_tup))->relkind == RELKIND_RELATION ||
		   ((Form_pg_class) GETSTRUCT(fc_tup))->relkind == RELKIND_MATVIEW ||
		   ((Form_pg_class) GETSTRUCT(fc_tup))->relkind == RELKIND_TOASTVALUE);

	fc_relopts = extractRelOptions(fc_tup, pg_class_desc, NULL);
	if (fc_relopts == NULL)
		return NULL;

	fc_av = palloc(sizeof(AutoVacOpts));
	memcpy(fc_av, &(((StdRdOptions *) fc_relopts)->autovacuum), sizeof(AutoVacOpts));
	pfree(fc_relopts);

	return fc_av;
}


/*
 * table_recheck_autovac
 *
 * 重新检查表是否仍需要清理或分析。返回值是一个有效的 autovac_table 指针，如果不需要则返回 NULL。
 *
 * 请注意，返回的 autovac_table 没有设置名称字段。
 */
static autovac_table *
fc_table_recheck_autovac(Oid fc_relid, HTAB *fc_table_toast_map,
					  TupleDesc pg_class_desc,
					  int fc_effective_multixact_freeze_max_age)
{
	Form_pg_class fc_classForm;
	HeapTuple	fc_classTup;
	bool		fc_dovacuum;
	bool		fc_doanalyze;
	autovac_table *fc_tab = NULL;
	bool		fc_wraparound;
	AutoVacOpts *fc_avopts;

	/* 获取关系的 relcache 条目 */
	fc_classTup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(fc_relid));
	if (!HeapTupleIsValid(fc_classTup))
		return NULL;
	fc_classForm = (Form_pg_class) GETSTRUCT(fc_classTup);

	/*
	 * 获取适用的 reloptions。如果是 TOAST 表，如果 toast 表本身没有，则尝试获取主表的 reloptions。
	 */
	fc_avopts = fc_extract_autovac_opts(fc_classTup, pg_class_desc);
	if (fc_classForm->relkind == RELKIND_TOASTVALUE &&
		fc_avopts == NULL && fc_table_toast_map != NULL)
	{
		av_relation *fc_hentry;
		bool		fc_found;

		fc_hentry = hash_search(fc_table_toast_map, &fc_relid, HASH_FIND, &fc_found);
		if (fc_found && fc_hentry->ar_hasrelopts)
			fc_avopts = &fc_hentry->ar_reloptions;
	}

	fc_recheck_relation_needs_vacanalyze(fc_relid, fc_avopts, fc_classForm,
									  fc_effective_multixact_freeze_max_age,
									  &fc_dovacuum, &fc_doanalyze, &fc_wraparound);

	/* 好吧，它需要进行一些操作 */
	if (fc_doanalyze || fc_dovacuum)
	{
		int			fc_freeze_min_age;
		int			fc_freeze_table_age;
		int			fc_multixact_freeze_min_age;
		int			fc_multixact_freeze_table_age;
		int			fc_vac_cost_limit;
		double		fc_vac_cost_delay;
		int			fc_log_min_duration;

		/*
		 * 计算 vacuum 成本参数和 freeze ages。如果 pg_class.reloptions 中设置了选项，则使用它们；在 toaster 表的情况下，也尝试主表。否则使用 GUC 默认值，autovacuum 的优先用法第一个，plain vacuum 第二个。
		 */

		/* autovac 设置中的 -1 意味着使用 plain vacuum_cost_delay */
		fc_vac_cost_delay = (fc_avopts && fc_avopts->vacuum_cost_delay >= 0)
			? fc_avopts->vacuum_cost_delay
			: (autovacuum_vac_cost_delay >= 0)
			? autovacuum_vac_cost_delay
			: VacuumCostDelay;

		/* autovac 设置中的 0 或 -1 意味着使用 plain vacuum_cost_limit */
		fc_vac_cost_limit = (fc_avopts && fc_avopts->vacuum_cost_limit > 0)
			? fc_avopts->vacuum_cost_limit
			: (autovacuum_vac_cost_limit > 0)
			? autovacuum_vac_cost_limit
			: VacuumCostLimit;

		/* autovac 设置中的 -1 意味着使用 log_autovacuum_min_duration */
		fc_log_min_duration = (fc_avopts && fc_avopts->log_min_duration >= 0)
			? fc_avopts->log_min_duration
			: Log_autovacuum_min_duration;

		/* 这些没有 autovacuum 特定设置 */
		fc_freeze_min_age = (fc_avopts && fc_avopts->freeze_min_age >= 0)
			? fc_avopts->freeze_min_age
			: default_freeze_min_age;

		fc_freeze_table_age = (fc_avopts && fc_avopts->freeze_table_age >= 0)
			? fc_avopts->freeze_table_age
			: default_freeze_table_age;

		fc_multixact_freeze_min_age = (fc_avopts &&
									fc_avopts->multixact_freeze_min_age >= 0)
			? fc_avopts->multixact_freeze_min_age
			: default_multixact_freeze_min_age;

		fc_multixact_freeze_table_age = (fc_avopts &&
									  fc_avopts->multixact_freeze_table_age >= 0)
			? fc_avopts->multixact_freeze_table_age
			: default_multixact_freeze_table_age;

		fc_tab = palloc(sizeof(autovac_table));
		fc_tab->at_relid = fc_relid;
		fc_tab->at_sharedrel = fc_classForm->relisshared;

		/* 注意，这跳过了 toast 表关系 */
		fc_tab->at_params.options = (fc_dovacuum ? VACOPT_VACUUM : 0) |
			(fc_doanalyze ? VACOPT_ANALYZE : 0) |
			(!fc_wraparound ? VACOPT_SKIP_LOCKED : 0);

		/*
		 * index_cleanup 和 truncate 在 autovacuum 中最初未指定。稍后将使用它们的 reloptions（或 reloption 默认值）填充可用值。
		 */
		fc_tab->at_params.index_cleanup = VACOPTVALUE_UNSPECIFIED;
		fc_tab->at_params.truncate = VACOPTVALUE_UNSPECIFIED;
		/* 到目前为止，我们不支持 autovacuum 的并行 vacuum */
		fc_tab->at_params.nworkers = -1;
		fc_tab->at_params.freeze_min_age = fc_freeze_min_age;
		fc_tab->at_params.freeze_table_age = fc_freeze_table_age;
		fc_tab->at_params.multixact_freeze_min_age = fc_multixact_freeze_min_age;
		fc_tab->at_params.multixact_freeze_table_age = fc_multixact_freeze_table_age;
		fc_tab->at_params.is_wraparound = fc_wraparound;
		fc_tab->at_params.log_min_duration = fc_log_min_duration;
		fc_tab->at_vacuum_cost_limit = fc_vac_cost_limit;
		fc_tab->at_vacuum_cost_delay = fc_vac_cost_delay;
		fc_tab->at_relname = NULL;
		fc_tab->at_nspname = NULL;
		fc_tab->at_datname = NULL;

		/*
		 * 如果为此表单独设置了任何成本延迟参数，则禁用平衡算法。
		 */
		fc_tab->at_dobalance =
			!(fc_avopts && (fc_avopts->vacuum_cost_limit > 0 ||
						 fc_avopts->vacuum_cost_delay >= 0));
	}

	heap_freetuple(fc_classTup);
	return fc_tab;
}

/*
 * recheck_relation_needs_vacanalyze
 *
 * table_recheck_autovac 的子例程。
 *
 * 获取关系的 pgstat 并重新检查关系是否需要进行 vacuum 或分析。
 */
static void fc_recheck_relation_needs_vacanalyze(Oid fc_relid,
								  AutoVacOpts *fc_avopts,
								  Form_pg_class fc_classForm,
								  int fc_effective_multixact_freeze_max_age,
								  bool *fc_dovacuum,
								  bool *fc_doanalyze,
								  bool *fc_wraparound)
{
	PgStat_StatTabEntry *fc_tabentry;

	/* 获取 pgstat 表条目 */
	fc_tabentry = pgstat_fetch_stat_tabentry_ext(fc_classForm->relisshared,
											  fc_relid);

	fc_relation_needs_vacanalyze(fc_relid, fc_avopts, fc_classForm, fc_tabentry,
							  fc_effective_multixact_freeze_max_age,
							  fc_dovacuum, fc_doanalyze, fc_wraparound);

	/* 忽略 toast 表的 ANALYZE */
	if (fc_classForm->relkind == RELKIND_TOASTVALUE)
		*fc_doanalyze = false;
}

/*
 * relation_needs_vacanalyze
 *
 * 检查关系是否需要进行 vacuum 或分析；将各自结果返回到 "dovacuum" 和 "doanalyze"。还返回 vacuum 是否因 Xid 或 multixact 包装而被强制。
 *
 * relopts 是指向 AutoVacOpts 选项的指针（在普通表的情况下为自身，或在 TOAST 表的情况下为自身或其父表），如果没有则为 NULL；tabentry 是 pgstats 条目，可以是 NULL。
 *
 * 如果死元组的数量超过阈值，则需要对表进行 vacuum。该阈值的计算方式为
 *
 * threshold = vac_base_thresh + vac_scale_factor * reltuples
 *
 * 对于分析，所做的分析是自上次分析以来插入、删除和更新的元组数量超过通过上述相同方式计算出的阈值。请注意，累积统计系统存储的是截至上次分析时的元组数量（包括活动和死元组）。这与 VACUUM 的情况不对称。
 *
 * 如果表的 relfrozenxid 超过 freeze_max_age 的事务数，并且其 relminmxid 超过 multixact_freeze_max_age 的多重事务数，则我们也强制进行 vacuum。
 *
 * autovacuum_enabled 选项为 false 的表会被自动跳过（除非由于 freeze_max_age 必须对其进行 vacuum）。因此，可以为特定表禁用 autovacuum。此外，当累积统计系统没有关于表的数据时，将跳过该表。
 *
 * vac_base_thresh 值 < 0 的表从 autovacuum_vacuum_threshold GUC 变量获取基础值。同样，vac_scale_factor 值 < 0 将被替换为 autovacuum_vacuum_scale_factor GUC 变量的值。分析也是如此。
 */
static void fc_relation_needs_vacanalyze(Oid fc_relid,
						  AutoVacOpts *fc_relopts,
						  Form_pg_class fc_classForm,
						  PgStat_StatTabEntry *fc_tabentry,
						  int fc_effective_multixact_freeze_max_age,
 /* 输出参数如下 */
						  bool *fc_dovacuum,
						  bool *fc_doanalyze,
						  bool *fc_wraparound)
{
	bool		fc_force_vacuum;
	bool		fc_av_enabled;
	float4		fc_reltuples;		/* pg_class.reltuples */

	/* 来自 reloptions 或 GUC 变量的常数 */
	int			fc_vac_base_thresh,
				fc_vac_ins_base_thresh,
				fc_anl_base_thresh;
	float4		fc_vac_scale_factor,
				fc_vac_ins_scale_factor,
				fc_anl_scale_factor;

	/* 从上述常数计算的阈值 */
	float4		fc_vacthresh,
				fc_vacinsthresh,
				fc_anlthresh;

	/* 此时的 vacuum（响应分析）元组数量 */
	float4		fc_vactuples,
				fc_instuples,
				fc_anltuples;

	/* 冻结参数 */
	int			fc_freeze_max_age;
	int			fc_multixact_freeze_max_age;
	TransactionId fc_xidForceLimit;
	MultiXactId fc_multiForceLimit;

	AssertArg(fc_classForm != NULL);
	AssertArg(OidIsValid(fc_relid));

	/*
	 * 确定 vacuum/analyze 方程参数。我们有两个可能的来源：传递的 reloptions（可以是主表或 toast 表），或 autovacuum GUC 变量。
	 */

	/* autovac 设置中的 -1 意味着使用 plain vacuum_scale_factor */
	fc_vac_scale_factor = (fc_relopts && fc_relopts->vacuum_scale_factor >= 0)
		? fc_relopts->vacuum_scale_factor
		: autovacuum_vac_scale;

	fc_vac_base_thresh = (fc_relopts && fc_relopts->vacuum_threshold >= 0)
		? fc_relopts->vacuum_threshold
		: autovacuum_vac_thresh;

	fc_vac_ins_scale_factor = (fc_relopts && fc_relopts->vacuum_ins_scale_factor >= 0)
		? fc_relopts->vacuum_ins_scale_factor
		: autovacuum_vac_ins_scale;

	/* -1 用于禁用插入 vacuum */
	fc_vac_ins_base_thresh = (fc_relopts && fc_relopts->vacuum_ins_threshold >= -1)
		? fc_relopts->vacuum_ins_threshold
		: autovacuum_vac_ins_thresh;

	fc_anl_scale_factor = (fc_relopts && fc_relopts->analyze_scale_factor >= 0)
		? fc_relopts->analyze_scale_factor
		: autovacuum_anl_scale;

	fc_anl_base_thresh = (fc_relopts && fc_relopts->analyze_threshold >= 0)
		? fc_relopts->analyze_threshold
		: autovacuum_anl_thresh;

	fc_freeze_max_age = (fc_relopts && fc_relopts->freeze_max_age >= 0)
		? Min(fc_relopts->freeze_max_age, autovacuum_freeze_max_age)
		: autovacuum_freeze_max_age;

	fc_multixact_freeze_max_age = (fc_relopts && fc_relopts->multixact_freeze_max_age >= 0)
		? Min(fc_relopts->multixact_freeze_max_age, fc_effective_multixact_freeze_max_age)
		: fc_effective_multixact_freeze_max_age;

	fc_av_enabled = (fc_relopts ? fc_relopts->enabled : true);

	/* 如果表面临包装风险则强制进行 vacuum */
	fc_xidForceLimit = recentXid - fc_freeze_max_age;
	if (fc_xidForceLimit < FirstNormalTransactionId)
		fc_xidForceLimit -= FirstNormalTransactionId;
	fc_force_vacuum = (TransactionIdIsNormal(fc_classForm->relfrozenxid) &&
					TransactionIdPrecedes(fc_classForm->relfrozenxid,
										  fc_xidForceLimit));
	if (!fc_force_vacuum)
	{
		fc_multiForceLimit = recentMulti - fc_multixact_freeze_max_age;
		if (fc_multiForceLimit < FirstMultiXactId)
			fc_multiForceLimit -= FirstMultiXactId;
		fc_force_vacuum = MultiXactIdIsValid(fc_classForm->relminmxid) &&
			MultiXactIdPrecedes(fc_classForm->relminmxid, fc_multiForceLimit);
	}
	*fc_wraparound = fc_force_vacuum;

	/* 用户在 pg_class.reloptions 中禁用了它？（但如果面临风险则忽略） */
	if (!fc_av_enabled && !fc_force_vacuum)
	{
		*fc_doanalyze = false;
		*fc_dovacuum = false;
		return;
	}

	/*
	 * 如果我们找到了表的状态，并且 autovacuum 当前处于启用状态，则基于阈值做出是否进行 vacuum 和/或 分析的决策。如果 autovacuum 当前已禁用，我们必须待在这里仅进行防止包装的 vacuum，因此不要 vacuum（或分析）任何未被强制的内容。
	 */
	if (PointerIsValid(fc_tabentry) && AutoVacuumingActive())
	{
		fc_reltuples = fc_classForm->reltuples;
		fc_vactuples = fc_tabentry->n_dead_tuples;
		fc_instuples = fc_tabentry->inserts_since_vacuum;
		fc_anltuples = fc_tabentry->changes_since_analyze;

		/* 如果表尚未进行 vacuum，则取 reltuples 为零 */
		if (fc_reltuples < 0)
			fc_reltuples = 0;

		fc_vacthresh = (float4) fc_vac_base_thresh + fc_vac_scale_factor * fc_reltuples;
		fc_vacinsthresh = (float4) fc_vac_ins_base_thresh + fc_vac_ins_scale_factor * fc_reltuples;
		fc_anlthresh = (float4) fc_anl_base_thresh + fc_anl_scale_factor * fc_reltuples;

		/*
		 * 请注意，我们不需要特别考虑统计重置，因为如果发生这种情况，上次 vacuum 和分析计数也将被重置。
		 */
		if (fc_vac_ins_base_thresh >= 0)
			elog(DEBUG3, "%s: vac: %.0f (threshold %.0f), ins: %.0f (threshold %.0f), anl: %.0f (threshold %.0f)",
				 NameStr(fc_classForm->relname),
				 fc_vactuples, fc_vacthresh, fc_instuples, fc_vacinsthresh, fc_anltuples, fc_anlthresh);
		else
			elog(DEBUG3, "%s: vac: %.0f (threshold %.0f), ins: (disabled), anl: %.0f (threshold %.0f)",
				 NameStr(fc_classForm->relname),
				 fc_vactuples, fc_vacthresh, fc_anltuples, fc_anlthresh);

		/* 确定此表是否需要 vacuum 或分析。 */
		*fc_dovacuum = fc_force_vacuum || (fc_vactuples > fc_vacthresh) ||
			(fc_vac_ins_base_thresh >= 0 && fc_instuples > fc_vacinsthresh);
		*fc_doanalyze = (fc_anltuples > fc_anlthresh);
	}
	else
	{
		/*
		 * 跳过未在统计哈希中找到的表，除非我们必须强制进行真空处理以防止溢出。
		 * 如果没有进行操作，就没有必要进行真空处理。
		 */
		*fc_dovacuum = fc_force_vacuum;
		*fc_doanalyze = false;
	}

	/* ANALYZE 拒绝与 pg_statistic 一起工作 */
	if (fc_relid == StatisticRelationId)
		*fc_doanalyze = false;
}

/*
 * autovacuum_do_vac_analyze
 *		对指定表进行真空和/或分析
 */
static void fc_autovacuum_do_vac_analyze(autovac_table *fc_tab, BufferAccessStrategy fc_bstrategy)
{
	RangeVar   *fc_rangevar;
	VacuumRelation *fc_rel;
	List	   *fc_rel_list;

	/* 让 pgstat 知道我们正在做什么 */
	fc_autovac_report_activity(fc_tab);

	/* 为 vacuum() 设置一个 VacuumRelation 目标，通过 OID 进行识别 */
	fc_rangevar = makeRangeVar(fc_tab->at_nspname, fc_tab->at_relname, -1);
	fc_rel = makeVacuumRelation(fc_rangevar, fc_tab->at_relid, NIL);
	fc_rel_list = list_make1(fc_rel);

	vacuum(fc_rel_list, &fc_tab->at_params, fc_bstrategy, true);
}

/*
 * autovac_report_activity
 *		向 pgstat 报告 autovacuum 当前正在做的事情
 *
 * 我们发送一个 SQL 字符串，对应于用户如果手动发出等效命令时所看到的内容。
 *
 * 注意我们假设在完成当前命令后会立即报告下一个命令，
 * 并在最后一个命令后退出，因此我们不会麻烦地报告 "<IDLE>" 或其他类似内容。
 */
static void fc_autovac_report_activity(autovac_table *fc_tab)
{
#define MAX_AUTOVAC_ACTIV_LEN (NAMEDATALEN * 2 + 56)
	char		fc_activity[MAX_AUTOVAC_ACTIV_LEN];
	int			fc_len;

	/* 报告命令及其可能的选项 */
	if (fc_tab->at_params.options & VACOPT_VACUUM)
		snprintf(fc_activity, MAX_AUTOVAC_ACTIV_LEN,
				 "autovacuum: VACUUM%s",
				 fc_tab->at_params.options & VACOPT_ANALYZE ? " ANALYZE" : "");
	else
		snprintf(fc_activity, MAX_AUTOVAC_ACTIV_LEN,
				 "autovacuum: ANALYZE");

	/*
	 * 报告关系的合格名称。
	 */
	fc_len = strlen(fc_activity);

	snprintf(fc_activity + fc_len, MAX_AUTOVAC_ACTIV_LEN - fc_len,
			 " %s.%s%s", fc_tab->at_nspname, fc_tab->at_relname,
			 fc_tab->at_params.is_wraparound ? " (to prevent wraparound)" : "");

	/* 将 statement_timestamp() 设置为 pg_stat_activity 的当前时间 */
	SetCurrentStatementStartTimestamp();

	pgstat_report_activity(STATE_RUNNING, fc_activity);
}

/*
 * autovac_report_workitem
 *		向 pgstat 报告 autovacuum 正在处理一个工作项
 */
static void fc_autovac_report_workitem(AutoVacuumWorkItem *fc_workitem,
						const char *fc_nspname, const char *fc_relname)
{
	char		fc_activity[MAX_AUTOVAC_ACTIV_LEN + 12 + 2];
	char		fc_blk[12 + 2];
	int			fc_len;

	switch (fc_workitem->avw_type)
	{
		case AVW_BRINSummarizeRange:
			snprintf(fc_activity, MAX_AUTOVAC_ACTIV_LEN,
					 "autovacuum: BRIN summarize");
			break;
	}

	/*
	 * 报告关系的合格名称，以及任何块号（如果有的话）
	 */
	fc_len = strlen(fc_activity);

	if (BlockNumberIsValid(fc_workitem->avw_blockNumber))
		snprintf(fc_blk, sizeof(fc_blk), " %u", fc_workitem->avw_blockNumber);
	else
		fc_blk[0] = '\0';

	snprintf(fc_activity + fc_len, MAX_AUTOVAC_ACTIV_LEN - fc_len,
			 " %s.%s%s", fc_nspname, fc_relname, fc_blk);

	/* 将 statement_timestamp() 设置为 pg_stat_activity 的当前时间 */
	SetCurrentStatementStartTimestamp();

	pgstat_report_activity(STATE_RUNNING, fc_activity);
}

/*
 * AutoVacuumingActive
 *		检查 GUC 变量并报告是否应运行 autovacuum 进程。
 */
bool AutoVacuumingActive(void)
{
	if (!autovacuum_start_daemon || !pgstat_track_counts)
		return false;
	return true;
}

/*
 * 请求一个工作项以处理我们数据库的下一个 autovacuum 运行。
 * 如果请求无法记录，则返回 false。
 */
bool AutoVacuumRequestWork(AutoVacuumWorkItemType fc_type, Oid fc_relationId,
					  BlockNumber fc_blkno)
{
	int			fc_i;
	bool		fc_result = false;

	LWLockAcquire(AutovacuumLock, LW_EXCLUSIVE);

	/*
	 * 找到一个未使用的工作项并使用给定数据填充它。
	 */
	for (fc_i = 0; fc_i < NUM_WORKITEMS; fc_i++)
	{
		AutoVacuumWorkItem *fc_workitem = &AutoVacuumShmem->av_workItems[fc_i];

		if (fc_workitem->avw_used)
			continue;

		fc_workitem->avw_used = true;
		fc_workitem->avw_active = false;
		fc_workitem->avw_type = fc_type;
		fc_workitem->avw_database = MyDatabaseId;
		fc_workitem->avw_relation = fc_relationId;
		fc_workitem->avw_blockNumber = fc_blkno;
		fc_result = true;

		/* 完成 */
		break;
	}

	LWLockRelease(AutovacuumLock);

	return fc_result;
}

/*
 * autovac_init
 *		在主进程初始化时调用。
 *
 * 我们在这里所做的只是让用户感到烦恼，如果他搞错了。
 */
void autovac_init(void)
{
	if (autovacuum_start_daemon && !pgstat_track_counts)
		ereport(WARNING,
				(errmsg("autovacuum not started because of misconfiguration"),
				 errhint("Enable the \"track_counts\" option.")));
}

/*
 * IsAutoVacuum functions
 *		返回这是否是启动器 autovacuum 进程或工作进程。
 */
bool IsAutoVacuumLauncherProcess(void)
{
	return am_autovacuum_launcher;
}

bool IsAutoVacuumWorkerProcess(void)
{
	return am_autovacuum_worker;
}


/*
 * AutoVacuumShmemSize
 *		计算 autovacuum 相关共享内存所需的空间
 */
Size AutoVacuumShmemSize(void)
{
	Size		fc_size;

	/*
	 * 需要固定结构和 WorkerInfoData 的数组。
	 */
	fc_size = sizeof(AutoVacuumShmemStruct);
	fc_size = MAXALIGN(fc_size);
	fc_size = add_size(fc_size, mul_size(autovacuum_max_workers,
								   sizeof(WorkerInfoData)));
	return fc_size;
}

/*
 * AutoVacuumShmemInit
 *		分配和初始化 autovacuum 相关的共享内存
 */
void AutoVacuumShmemInit(void)
{
	bool		fc_found;

	AutoVacuumShmem = (AutoVacuumShmemStruct *)
		ShmemInitStruct("AutoVacuum Data",
						AutoVacuumShmemSize(),
						&fc_found);

	if (!IsUnderPostmaster)
	{
		WorkerInfo	fc_worker;
		int			fc_i;

		Assert(!fc_found);

		AutoVacuumShmem->av_launcherpid = 0;
		dlist_init(&AutoVacuumShmem->av_freeWorkers);
		dlist_init(&AutoVacuumShmem->av_runningWorkers);
		AutoVacuumShmem->av_startingWorker = NULL;
		memset(AutoVacuumShmem->av_workItems, 0,
			   sizeof(AutoVacuumWorkItem) * NUM_WORKITEMS);

		fc_worker = (WorkerInfo) ((char *) AutoVacuumShmem +
							   MAXALIGN(sizeof(AutoVacuumShmemStruct)));

		/* 初始化 WorkerInfo 空闲列表 */
		for (fc_i = 0; fc_i < autovacuum_max_workers; fc_i++)
			dlist_push_head(&AutoVacuumShmem->av_freeWorkers,
							&fc_worker[fc_i].wi_links);
	}
	else
		Assert(fc_found);
}
